Skip to content

fix(go): correct Permissions serialization and add unit tests#3015

Open
saie-ch wants to merge 8 commits intoapache:masterfrom
saie-ch:go_issues
Open

fix(go): correct Permissions serialization and add unit tests#3015
saie-ch wants to merge 8 commits intoapache:masterfrom
saie-ch:go_issues

Conversation

@saie-ch
Copy link
Copy Markdown
Contributor

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

Bugs Fixed:

Permissions.MarshalBinary() - Fixed missing continuation flags between stream/topic entries. The serializer wasn't writing 1-byte flags
(1=has_next, 0=no_next) after each entry, causing the server to incorrectly parse permission data and potentially grant wrong permissions. Also
added len() > 0 checks to match Rust SDK's behavior of skipping empty maps.

Note: Does NOT fix #2980, #2981, #2982 (missing 4-byte permissions_len field before permissions data - those are separate issues found during the review of #2973).

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.95%. Comparing base (5cc7c37) to head (0fe9765).
⚠️ Report is 16 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3015      +/-   ##
============================================
+ Coverage     71.88%   71.95%   +0.06%     
  Complexity      930      930              
============================================
  Files          1118     1120       +2     
  Lines         92992    93179     +187     
  Branches      70513    70513              
============================================
+ Hits          66851    67043     +192     
+ Misses        23582    23578       -4     
+ Partials       2559     2558       -1     
Flag Coverage Δ
go 38.97% <100.00%> (+2.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 100.00% <100.00%> (+7.57%) ⬆️

... and 3 files 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.

@saie-ch
Copy link
Copy Markdown
Contributor Author

saie-ch commented Mar 23, 2026

@chengxilo could you please check this?

@hubcio
Copy link
Copy Markdown
Contributor

hubcio commented Mar 23, 2026

PR description is not good enough. please make it more meaningful: what bug was that, how did it manifest and how was it fixed. prefer on describing why instead of what.

Copy link
Copy Markdown
Member

@ryankert01 ryankert01 left a comment

Choose a reason for hiding this comment

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

I think it's better to add a test to prevent regression

@saie-ch
Copy link
Copy Markdown
Contributor Author

saie-ch commented Mar 23, 2026

I think it's better to add a test to prevent regression

we are adding tests in #2973

@hubcio
Copy link
Copy Markdown
Contributor

hubcio commented Mar 23, 2026

@saie-ch so why these tests passed? do those commits overlap?

@saie-ch
Copy link
Copy Markdown
Contributor Author

saie-ch commented Mar 23, 2026

@saie-ch so why these tests passed? do those commits overlap?

@hubcio Thanks for the question! Could you clarify which tests you're referring to?
For context: The tests passed in #2973 because the bugs were already fixed in that same PR As per @chengxilo review comment, we separated the bug fixes into this PR first

@chengxilo
Copy link
Copy Markdown
Contributor

chengxilo commented Mar 23, 2026

I think it's better to add a test to prevent regression

we are adding tests in #2973

In this PR, you better only address the problem of Permissions

Permissions.MarshalBinary() - Fixed missing continuation flags between stream/topic entries. The serializer wasn't writing 1-byte flags (1=has_next, 0=no_next) after each entry, causing the server to incorrectly parse permission data and potentially grant wrong permissions. Also added len() > 0 checks to match Rust SDK's behavior of skipping empty maps.

For the following changes related to commands, it would be better to address them in #2973, since they were discovered while you implementing command tests(and more importantly, they are logic of commands):

CreateUser - Fixed capacity calculation that didn't account for individual length prefix bytes. Was allocating 4 + len(username) + len(password) but needed 1 + len(username) + 1 + len(password) + 1 + 1 for proper byte accounting. Also removed double-counted permissions flag byte.

UpdatePermissions - Fixed panic when writing has_permissions flag. Buffer was allocated as len(userIdBytes) without the required 1-byte flag, causing index-out-of-bounds when accessing bytes[position]. Now allocates len(userIdBytes) + 1.

UpdateUser - Fixed panic from insufficient buffer allocation (missing flag bytes) and removed input mutation side effect. Method was modifying caller's struct by allocating new(string) when Username was nil. Now uses read-only access with proper buffer sizing: len(userIdBytes) + 2 for both flags.

Regarding this part

Note: Does NOT fix #2980, #2981, #2982 (missing 4-byte permissions_len field before permissions data - those are separate issues found during the review of #2973).

I’m still a bit confused why we tend to separate bug fixes and tests into different PRs. In many cases, writing tests alongside the fix makes it easier to validate the implementation immediately ^v^.

@chengxilo
Copy link
Copy Markdown
Contributor

@saie-ch so why these tests passed? do those commits overlap?

@hubcio Thanks for the question! Could you clarify which tests you're referring to? For context: The tests passed in #2973 because the bugs were already fixed in that same PR As per @chengxilo review comment, we separated the bug fixes into this PR first

Sorry for making you confused, actually you just need to use a seperate PR to solve the problem of iggon.Permissions since it's not in the scope of that PR. 😢

@chengxilo
Copy link
Copy Markdown
Contributor

@saie-ch so why these tests passed? do those commits overlap?

@hubcio Thanks for the question! Could you clarify which tests you're referring to? For context: The tests passed in #2973 because the bugs were already fixed in that same PR As per @chengxilo review comment, we separated the bug fixes into this PR first

Also I think the tests @hubcio refering to is the tests you implemented for command. As those tests passed it bascially means they didn't detect the bug in iggon.Permission , that's why @hubcio and @ryankert01 are requesting you to add a test, because we don't have a proper test for the iggon.Permission.

@saie-ch
Copy link
Copy Markdown
Contributor Author

saie-ch commented Mar 23, 2026

@chengxilo I misunderstood the original review feedback and thought the suggestion was to separate ALL bug fixes from the tests.

Now:
Permissions fix -> Should be in a separate PR (this one).
Command fixes -> Should stay in #2973
I'll add a unit test for permissions in this PR too.

Copy link
Copy Markdown
Contributor

@chengxilo chengxilo left a comment

Choose a reason for hiding this comment

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

Ohter part looks good to me

Comment on lines +25 to +151
func TestPermissions_MarshalBinary_WithStreamsAndTopics(t *testing.T) {
// Test case: Permissions with 2 streams, first stream has 2 topics, second has none
permissions := &Permissions{
Global: GlobalPermissions{
ManageServers: true,
ReadServers: false,
ManageUsers: true,
ReadUsers: false,
ManageStreams: true,
ReadStreams: false,
ManageTopics: true,
ReadTopics: false,
PollMessages: true,
SendMessages: false,
},
Streams: map[int]*StreamPermissions{
1: {
ManageStream: true,
ReadStream: false,
ManageTopics: true,
ReadTopics: false,
PollMessages: true,
SendMessages: false,
Topics: map[int]*TopicPermissions{
10: {
ManageTopic: true,
ReadTopic: false,
PollMessages: true,
SendMessages: false,
},
20: {
ManageTopic: false,
ReadTopic: true,
PollMessages: false,
SendMessages: true,
},
},
},
2: {
ManageStream: false,
ReadStream: true,
ManageTopics: false,
ReadTopics: true,
PollMessages: false,
SendMessages: true,
Topics: nil,
},
},
}

bytes, err := permissions.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary failed: %v", err)
}

// Verify structure
position := 0

// Global permissions (10 bytes)
if bytes[position] != 1 {
t.Errorf("Expected ManageServers=1, got %d", bytes[position])
}
position += 10

// Has streams flag
if bytes[position] != 1 {
t.Errorf("Expected has_streams=1, got %d", bytes[position])
}
position++

// Verify continuation flags are present for streams
// We should have 2 streams, so we need to find stream continuation flags
streamsFound := 0
for position < len(bytes) {
// Each stream has: 4 bytes (ID) + 6 bytes (perms) + 1 byte (has_topics) + topics + 1 byte (has_next_stream)
if position+4 > len(bytes) {
break
}
streamID := binary.LittleEndian.Uint32(bytes[position : position+4])
if streamID == 0 {
break
}
position += 4 // stream ID
position += 6 // stream permissions
position += 1 // has_topics flag

// Skip topics if present
if bytes[position-1] == 1 {
// Topics exist, need to skip them
for position+4 <= len(bytes) {
position += 4 // topic ID
position += 4 // topic permissions
if position >= len(bytes) {
t.Fatalf("Unexpected end of bytes while reading topic continuation flag")
}
hasNextTopic := bytes[position]
position++
if hasNextTopic == 0 {
break
}
}
}

// Check stream continuation flag
if position >= len(bytes) {
t.Fatalf("Unexpected end of bytes while reading stream continuation flag")
}
hasNextStream := bytes[position]
position++
streamsFound++

if streamsFound == 1 && hasNextStream != 1 {
t.Errorf("Expected has_next_stream=1 for first stream, got %d", hasNextStream)
}
if streamsFound == 2 && hasNextStream != 0 {
t.Errorf("Expected has_next_stream=0 for second stream, got %d", hasNextStream)
}

if hasNextStream == 0 {
break
}
}

if streamsFound != 2 {
t.Errorf("Expected 2 streams, found %d", streamsFound)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@saie-ch The test should verify that each attribute of Permissions is encoded correctly. Right now, those checks are being skipped. This only validates the basic frame structure (e.g., continuation flags and the size of each part). It would be better to also read back the encoded content and confirm that it matches the values you originally provided.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

+1

@chengxilo
Copy link
Copy Markdown
Contributor

Btw would you mind to update the PR title, maybe fix(go): correct Permissions serialization and add unit tests? It would better reflect what you’re currently working on.

@saie-ch saie-ch changed the title fix(go): fix command serialization bugs fix(go): correct Permissions serialization and add unit tests Mar 24, 2026
Comment on lines 25 to 39
func TestPermissions_MarshalBinary_WithStreamsAndTopics(t *testing.T) {
// Test case: Permissions with 2 streams, first stream has 2 topics, second has none
permissions := &Permissions{
Global: GlobalPermissions{
ManageServers: true,
ReadServers: false,
ManageUsers: true,
ReadUsers: false,
ManageStreams: true,
ReadStreams: false,
ManageTopics: true,
ReadTopics: false,
PollMessages: true,
SendMessages: false,
},
Copy link
Copy Markdown
Contributor

@chengxilo chengxilo Mar 24, 2026

Choose a reason for hiding this comment

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

I'm sorry that I keep asking you to modify your code.

However, your changes actually doesn't address the problem. If in the future I swaped the position of ManageServers and ManageUsers when encoding, this test won't know I am wrong.

To better desribe the problem, consider you have this struct.

Foo {
    A bool,
    B bool,
    C bool,
}

You wrot a function to encode it.

func (f Foo) MarshalBinary() ([]byte, error) {
    return []byte{
        boolToByte(f.A), 
        boolToByte(f.B), 
        boolToByte(f.C),
    }
}

You wrote a test and here is your test case:

input: Foo {
    A: true,
    B: false,
    C: true
}
want: []byte {1, 0, 1}

And you can get a []byte {1, 0, 1}, you are 100% correct.

However, if I write the function like this:

func (f Foo) MarshalBinary() ([]byte, error) {
    return []byte{
        boolToByte(f.C), 
        boolToByte(f.B), 
        boolToByte(f.A),
    }
}

I will still get []byte {1, 0, 1}, I can still pass the test, but actually it's wrong.

So you probably want to make sure every single field are correctly encoded.

Consider make a bigger test case:

{ 
    input: Foo {
        A: true
    },
    want: []byte{1,0,0}
},
{
    input: Foo {
        B: true
    },
    want: []byte{0,1,0}
},
{
    input: Foo {
        C: true
    },
    want: []byte{0,0,1}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No problem @chengxilo, happy to modify until it's correct. Can you check now?

Comment on lines 49 to 62
10: {
ManageTopic: true,
ReadTopic: false,
PollMessages: true,
SendMessages: false,
},
20: {
ManageTopic: false,
ReadTopic: true,
PollMessages: false,
SendMessages: true,
},
},
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same here

@hubcio
Copy link
Copy Markdown
Contributor

hubcio commented Mar 26, 2026

@saie-ch please use precommit hooks via prek. See CONTRIBUTING.md in the root of this repository.

Copy link
Copy Markdown
Contributor

@chengxilo chengxilo left a comment

Choose a reason for hiding this comment

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

lgtm now

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: CreatePersonalAccessToken writes expiry as uint32 at wrong offset in uint64 field

4 participants