feat(go): add comprehensive unit tests for command serialization#2973
feat(go): add comprehensive unit tests for command serialization#2973saie-ch wants to merge 2 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2973 +/- ##
============================================
+ Coverage 73.78% 73.80% +0.01%
Complexity 943 943
============================================
Files 1200 1200
Lines 109094 109096 +2
Branches 85994 85994
============================================
+ Hits 80492 80515 +23
+ Misses 25866 25849 -17
+ Partials 2736 2732 -4
🚀 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
|
@chengxilo could you please also check this? |
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)?
| @@ -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. |
|
@saie-ch @chengxilo what's the status of this PR? is it ready to be merged? should #3015 be merged first? |
Yes please @hubcio, let's wait for that to be merged first. I just pushed new changes. |
|
This PR may still need some further review and improvement. We should merge #3015 first tho. |
|
#3015 is merged. |
|
@saie-ch @atharvalade what is the difference between this PR and #3037 ? |
This one is created a few days earlier and only covers the test for commands.🤕 |
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. If you need a review, please ensure CI is green and the PR is rebased on the latest master. Don't hesitate to ping the maintainers - either @core on Discord or by mentioning them directly here on the PR. Thank you for your contribution! |
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. If you need a review, please ensure CI is green and the PR is rebased on the latest master. Don't hesitate to ping the maintainers - either @core on Discord or by mentioning them directly here on the PR. Thank you for your contribution! |
Yes, please review. I believe, i've implemented the necessary changes. |
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. If you need a review, please ensure CI is green and the PR is rebased on the latest master. Don't hesitate to ping the maintainers - either @core on Discord or by mentioning them directly here on the PR. Thank you for your contribution! |
|
/ready |
|
@saie-ch I noticed that comments from this PR are not fixed (output from claude code): can you fix them? /author |
thanks @hubcio , will try to fix them. |
|
@hubcio Fixed #1 (Expiry uint32→uint64) and #4 (stop mutating receiver). Also fixed the same mutation pattern in CreateTopic and UpdateTopic, and added non-mutation tests per @chengxilo's ask. For #2 and #3 — the reviewer's suggestion to write permissions_len:u32_le=0 in the nil case is incorrect per the Rust reference. Both create_user.rs:63-64 and update_permissions.rs:53-54 only write put_u8(0) when permissions are None — no permissions_len field. The Rust test at create_user.rs:201 confirms: &[1,b'u', 1, b'p', 0, 0]. The real buffer bugs were already fixed by PR #3097. The Go code matches the Rust wire format as-is. |
|
you can use /ready at the beginning of the line to change label. /ready @saie-ch i will check this later in upcoming days. |
hubcio
left a comment
There was a problem hiding this comment.
a few items that don't map to changed lines in this diff:
-
foreign/go/contracts/users.goPermissions.MarshalBinaryiteratesp.Streamsandstream.Topics(go maps) without sorting keys, so the produced wire bytes are non-deterministic across runs. two calls on the samePermissionsvalue can produce different byte sequences. this file is not touched in this pr, but the new tests do not catch it either (since the structures used have either nil or single-entry maps). worth either fixing here (sort keys before iterating, e.g.slices.Sorted(maps.Keys(p.Streams))) or filing a follow-up. -
pr description says "Permissions.MarshalBinary() - Fixed first permission byte corruption when streams are nil" but
users.gois unchanged in this pr. that fix landed earlier in #3015 (169a09bcd). consider rewording to "verified by new tests" or dropping the claim. -
pr description says "CreateUser - Fixed off-by-one buffer allocation (extra trailing byte)" but old
capacity = 4 + N + Mand new1 + N + 1 + M + 1 + 1 = 4 + N + Mare arithmetically identical. the change is a readability refactor, not a bugfix.
| bytes[0] = byte(len(c.Name)) | ||
| copy(bytes[1:], c.Name) | ||
| binary.LittleEndian.PutUint32(bytes[len(bytes)-4:], c.Expiry) | ||
| binary.LittleEndian.PutUint64(bytes[1+len(c.Name):], c.Expiry) |
There was a problem hiding this comment.
copy(bytes[1:], c.Name) writes into a dest of size N+8 but only copies N bytes. works, but reads as if it could overflow. copy(bytes[1:1+len(c.Name)], c.Name) is clearer and lines up with the explicit bytes[1+len(c.Name):] index on the next line.
| if len(username) != 0 { | ||
| length += 2 + len(username) | ||
| length += 1 + len(username) | ||
| } |
There was a problem hiding this comment.
len(username) == 0 collapses Username: stringPtr("") to the same wire bytes as Username: nil (has-username=0). that means a caller cannot distinguish "clear the username" from "leave it untouched" - the server sees the latter in both cases. TestSerialize_UpdateUser_EmptyUsername locks this behavior in. is the collapse intentional? if yes, leave a comment explaining it; if no, gate on the pointer (u.Username != nil) instead of the string length.
| func buildExpectedLoginUser(username, password string) []byte { | ||
| versionBytes := []byte(iggcon.Version) | ||
| contextBytes := []byte("") | ||
|
|
||
| totalLength := 1 + len(username) + 1 + len(password) + | ||
| 4 + len(versionBytes) + 4 + len(contextBytes) | ||
|
|
||
| buf := make([]byte, totalLength) | ||
| pos := 0 | ||
|
|
||
| buf[pos] = byte(len(username)) | ||
| pos++ | ||
| copy(buf[pos:], username) | ||
| pos += len(username) | ||
|
|
||
| buf[pos] = byte(len(password)) | ||
| pos++ | ||
| copy(buf[pos:], password) | ||
| pos += len(password) | ||
|
|
||
| binary.LittleEndian.PutUint32(buf[pos:], uint32(len(versionBytes))) | ||
| pos += 4 | ||
| copy(buf[pos:], versionBytes) | ||
| pos += len(versionBytes) | ||
|
|
||
| binary.LittleEndian.PutUint32(buf[pos:], uint32(len(contextBytes))) | ||
| pos += 4 | ||
| copy(buf[pos:], contextBytes) | ||
|
|
||
| return buf | ||
| } |
There was a problem hiding this comment.
buildExpectedLoginUser rebuilds the expected bytes the same way the production code does (1-byte len prefix, iggcon.Version, same offsets). if prod is wrong, expected is wrong in lockstep and the assertion passes silently. consider either (a) hardcoding golden bytes for one fixed case with a known/substituted version, or (b) asserting each field individually against literal byte values instead of a mirror helper. the version case alone (TestSerialize_LoginUser_ContainsVersion) reads version from the same offset it was written at, so a wrong offset on both sides would still pass.
| perms := &iggcon.Permissions{ | ||
| Global: iggcon.GlobalPermissions{ | ||
| ManageServers: true, | ||
| ReadServers: true, | ||
| }, | ||
| } |
There was a problem hiding this comment.
snap here (and &u / &s / &rf / perms in the cases below) shares pointers with cmd. reflect.DeepEqual on the dereferenced struct values walks through pointers and compares the same backing memory, so a MarshalBinary that mutated *Permissions content (e.g. sorted Streams in place) or wrote through *Username would not be caught. the test only catches pointer reassignment - which is what the old t.ReplicationFactor = new(uint8) bug did, so it works for the fixes in this pr, but the name promises more than it delivers. deep-copy the receiver into snapshot (e.g. via a small helper that returns a value with freshly-allocated *string/*uint8/*Permissions) to actually verify no-mutation.
slbotbm
left a comment
There was a problem hiding this comment.
The changes look alright to me, though there is no validation for the input in most of the implementations that these tests test, and thus there are no negative tests. That means these will silently. This can be a follow-up PR.
Also, I noticed that there were tests testing serialization of empty strings. Aren't those supposed to be errors? Adding them shows that they are expected behaviour, not errors.
| cmd := CreatePersonalAccessToken{ | ||
| Name: "long_token", | ||
| Expiry: 18446744073709551615, // Max uint64 value |
There was a problem hiding this comment.
This is a magic number. The following can be used instead:
import "math"
x := uint64(math.MaxUint64)There was a problem hiding this comment.
It looks like internal/command/access_token.go does not have error paths for the commands, always returning bytes, nil or []bytes, nil. Thus cases like Name > 255 chars is not handled by the implementation and would not raise an error.
There was a problem hiding this comment.
Implementation could be a follow to this PR.
There was a problem hiding this comment.
Same for this file as access_token.go. Validation for things like Name > 255 chars is not happening.
While CreateConsumerGroup, GetConsumerGroups, etc. return an error, there is no meaningful local validation / error paths.
There was a problem hiding this comment.
Same as previously stated, no negative tests asserting MarshalBinary() errors.
| func TestSerialize_StoreConsumerOffsetRequest_WithPartition(t *testing.T) { | ||
| consumerId, _ := iggcon.NewIdentifier(uint32(42)) | ||
| streamId, _ := iggcon.NewIdentifier("stream1") |
There was a problem hiding this comment.
A test with max uint32 can also be added for StoreConsumerOffsetRequest
There was a problem hiding this comment.
Same pattern as the other files here - no negative/error-case tests here as well, and no validation in session.go
There was a problem hiding this comment.
Same as others - no negative/error-case tests and no validation in system.go.
| ClientID: 4294967295, // Max uint32 value | ||
| } |
There was a problem hiding this comment.
Same with this - no negative tests.
|
/author |
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: