Skip to content

feat(go): add comprehensive unit tests for command serialization#2973

Open
saie-ch wants to merge 1 commit intoapache:masterfrom
saie-ch:go_unit_tests
Open

feat(go): add comprehensive unit tests for command serialization#2973
saie-ch wants to merge 1 commit intoapache:masterfrom
saie-ch:go_unit_tests

Conversation

@saie-ch
Copy link
Contributor

@saie-ch saie-ch commented Mar 18, 2026

Which issue does this PR close?

Closes #2883

Rationale

The Go SDK contains numerous command types that implement the MarshalBinary interface 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:

  1. Permissions.MarshalBinary() - Fixed first permission byte corruption when streams are nil
  2. CreateUser - Fixed off-by-one buffer allocation (extra trailing byte)
  3. UpdatePermissions - Fixed panic when permissions are nil (missing flag byte allocation)
  4. UpdateUser - Fixed panic when both username and status are nil (insufficient buffer size)

Test Coverage:

  • Overall package: 75.9% coverage
  • Tested commands: 85-100% coverage per command
  • All tests pass with race detector enabled

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

  • Helped design comprehensive test coverage strategy
  • Discovered 4 serialization bugs through systematic edge case testing
  • Generated test structures following Go best practices
  • Assisted with cross-validation against Java and Rust SDK implementations
  • Provided guidance on fixing buffer allocation bugs

Verification:

  • All tests run successfully with race detector enabled
  • Compared implementation patterns with Rust SDK reference implementation

@codecov
Copy link

codecov bot commented Mar 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.01%. Comparing base (0102b50) to head (4fa2b67).

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     
Flag Coverage Δ
go 36.97% <100.00%> (+0.58%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
foreign/go/contracts/users.go 92.42% <100.00%> (ø)
foreign/go/internal/command/update_user.go 95.12% <100.00%> (+41.46%) ⬆️
foreign/go/internal/command/user.go 91.30% <100.00%> (+6.52%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@atharvalade atharvalade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall looking good but I found a few things that need to be addressed

Comment on lines +134 to +139
serialized, err := cmd.MarshalBinary()
if err != nil {
t.Fatalf("Failed to serialize CreatePersonalAccessToken with zero expiry: %v", err)
}

expected := []byte{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +197 to +209
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +408 to +431
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@atharvalade
Copy link
Contributor

atharvalade commented Mar 19, 2026

I found 3 pre-existing issues #2980 #2981 #2982
I will wait for this merge and @saie-ch's comments before starting work on the issues mentioned above.

@hubcio
Copy link
Contributor

hubcio commented Mar 19, 2026

@chengxilo could you please also check this?

bytes[position+3] = boolToByte(topic.SendMessages)
position += 4

bytes[position] = byte(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part should not use byte(0) directly, doesn't align with the behavior in rust sdk.

if current_stream < streams_count {
current_stream += 1;
bytes.put_u8(1);
} else {
bytes.put_u8(0);
}
}

@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)
Copy link
Contributor

@chengxilo chengxilo Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can solve this in the new PR too (sorry I thought it was a minor problem in the scope of this pr)

Comment on lines 39 to 43
@@ -43,14 +43,14 @@ func (u *UpdateUser) MarshalBinary() ([]byte, error) {
username := *u.Username
Copy link
Contributor

@chengxilo chengxilo Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@chengxilo
Copy link
Contributor

regarding #2973 (comment), I think it would be great to write tests to check whether they are modified after call MarshalBinary, for each command.

@saie-ch
Copy link
Contributor Author

saie-ch commented Mar 20, 2026

Thanks @atharvalade and @chengxilo, I will implement the necessary changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Go SDK: Add unit tests for command serialization/deserialization

4 participants