feat(go): add comprehensive unit tests for command serialization#2973
feat(go): add comprehensive unit tests for command serialization#2973saie-ch wants to merge 1 commit intoapache:masterfrom
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2973 +/- ##
============================================
+ Coverage 71.98% 72.01% +0.02%
Complexity 925 925
============================================
Files 1113 1113
Lines 92345 92345
Branches 69896 69896
============================================
+ Hits 66473 66500 +27
+ Misses 23314 23291 -23
+ Partials 2558 2554 -4
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
atharvalade
left a comment
There was a problem hiding this comment.
overall looking good but I found a few things that need to be addressed
| serialized, err := cmd.MarshalBinary() | ||
| if err != nil { | ||
| t.Fatalf("Failed to serialize CreatePersonalAccessToken with zero expiry: %v", err) | ||
| } | ||
|
|
||
| expected := []byte{ |
There was a problem hiding this comment.
These 4 zero bytes are not "Reserved/padding" -- the Rust wire format specifies [expiry:u64_le] starting at position 1 + name_len (see core/binary_protocol/.../create_personal_access_token.rs:25). The Go implementation uses PutUint32 at len(bytes)-4, which writes the expiry in the high 32 bits of the u64 field, causing the server to interpret the value as expiry << 32. The underlying bug is in access_token.go:36 -- it should use PutUint64(bytes[1+len(c.Name):], uint64(c.Expiry)), and the Expiry field type should be uint64 to match the wire protocol. These tests currently codify the buggy serialization.
| 0x75, 0x73, 0x65, 0x72, // "user" | ||
| 0x04, // Password length = 4 | ||
| 0x70, 0x61, 0x73, 0x73, // "pass" | ||
| 0x01, // Status = Active (1) | ||
| 0x01, // Has permissions = 1 | ||
| 0x0B, 0x00, 0x00, 0x00, // Permissions length = 11 | ||
| // Global permissions: 1,1,0,1,0,1,0,1,1,0 | ||
| 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, | ||
| 0x00, // No stream-specific permissions | ||
| } | ||
|
|
||
| if !bytes.Equal(serialized, expected) { | ||
| t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) |
There was a problem hiding this comment.
This expected output is missing the permissions_len:u32_le field. The Rust wire format (see create_user.rs:44-45) always includes permissions_len on the wire, even when has_permissions=0. The expected bytes should end with 0x01, 0x00, 0x00, 0x00, 0x00, 0x00 (status, has_permissions=0, permissions_len=0). The underlying bug is in user.go:77 where the nil-permissions branch only writes the flag byte but not the length field.
| 0x04, // Length = 4 | ||
| 0xE7, 0x03, 0x00, 0x00, // Value = 999 | ||
| } | ||
|
|
||
| if !bytes.Equal(serialized, expected) { | ||
| t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) | ||
| } | ||
| } | ||
|
|
||
| // TestSerialize_DeleteUser_StringId tests DeleteUser with string identifier | ||
| func TestSerialize_DeleteUser_StringId(t *testing.T) { | ||
| userId, _ := iggcon.NewIdentifier("test_user") | ||
|
|
||
| cmd := DeleteUser{ | ||
| Id: userId, | ||
| } | ||
|
|
||
| serialized, err := cmd.MarshalBinary() | ||
| if err != nil { | ||
| t.Fatalf("Failed to serialize DeleteUser with string ID: %v", err) | ||
| } | ||
|
|
||
| expected := []byte{ | ||
| 0x02, // Kind = StringId |
There was a problem hiding this comment.
This fix prevents the nil-permissions panic (good), but it's incomplete. The base length should be len(userIdBytes) + 1 + 4 because the Rust wire format always includes permissions_len:u32_le on the wire even when has_permissions=0 (see update_permissions.rs:37-39). As-is, the server will fail to decode the message because it reads 4 bytes for permissions_len that aren't present. The else branch at line 142 also needs to write permissions_len=0 (4 zero bytes) after the flag byte.
|
@chengxilo could you please also check this? |
| bytes[position+3] = boolToByte(topic.SendMessages) | ||
| position += 4 | ||
|
|
||
| bytes[position] = byte(0) |
There was a problem hiding this comment.
This part should not use byte(0) directly, doesn't align with the behavior in rust sdk.
iggy/core/common/src/types/permissions/permissions_global.rs
Lines 267 to 273 in 9c8b5cc
@saie-ch would you mind to solve this in another PR first (to Implement unit test for permission and fix the problem in permission)?
| } | ||
| } else { | ||
| bytes[0] = byte(0) | ||
| bytes[position] = byte(0) |
There was a problem hiding this comment.
You can solve this in the new PR too (sorry I thought it was a minor problem in the scope of this pr)
| @@ -43,14 +43,14 @@ func (u *UpdateUser) MarshalBinary() ([]byte, error) { | |||
| username := *u.Username | |||
There was a problem hiding this comment.
Here the u.Username is actually modified by the method, I don't think it's a good approach. Since you already modifed this method, would you mind to modified this too?
|
regarding #2973 (comment), I think it would be great to write tests to check whether they are modified after call |
|
Thanks @atharvalade and @chengxilo, I will implement the necessary changes. |
Which issue does this PR close?
Closes #2883
Rationale
The Go SDK contains numerous command types that implement the
MarshalBinaryinterface for binary serialization, but many lacked comprehensive unit tests. Without thorough testing, serialization bugs can cause data corruption, runtime panics, and wire protocol incompatibility with the Iggy server and other SDKs.What changed?
Before: Go SDK had only 6 basic tests for command serialization.
After: Added 75 new comprehensive unit tests (81 total) covering 36 commands with:
Bugs Fixed:
Test Coverage:
Local Execution
All 81 tests passed
Pre-commit hooks ran (format, tidy, generate check, vet, build)
Race detector enabled - no race conditions detected
Coverage: 75.9% in internal/command package
All pre-merge checks passed:
AI Usage
Tool: Claude Code (claude-sonnet-4.5)
Scope: Test implementation, bug discovery, and cross-SDK validation
Verification: