[Fix] nkey jetstream - #18
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a critical bug where NKey authentication failed for JetStream connections. The issue was caused by using an incorrect field name (nkey_pub instead of nkey) in the CONNECT message sent to the NATS server.
Key Changes:
- Fixed the field name in
ConnectMessagestruct fromnkey_pubtonkeyto align with NATS protocol specifications - Added comprehensive JetStream integration tests for all authentication methods (token, user/pass, nkey) to prevent similar issues
- Fixed memory leaks in consumer tests by properly placing
defer js.DISCONNECT()statements
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/protocol.zig | Corrected the NKey field name from nkey_pub to nkey in the ConnectMessage struct, enabling proper NKey authentication for JetStream |
| src/integration_tests.zig | Added JetStream test coverage for token, user/pass, and nkey authentication methods to verify end-to-end authentication flows |
| src/consumer_tests.zig | Fixed memory leaks by moving defer js.DISCONNECT() immediately after JetStream connection and simplified error handling |
| .github/workflows/ci.yml | Reorganized test execution order and updated NATS server ports to prevent conflicts between different test stages |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var stream_created = false; | ||
| js.CREATE(&stream_config) catch |err| { | ||
| if (err == error.StreamAlreadyExists) { | ||
| std.debug.print("Token auth (JetStream): Stream already exists (OK)\n", .{}); | ||
| stream_created = true; | ||
| } else { | ||
| std.debug.print("Token auth (JetStream): CREATE failed with error: {}\n", .{err}); | ||
| return err; | ||
| } | ||
| }; | ||
|
|
||
| if (!stream_created) { | ||
| stream_created = true; | ||
| } | ||
|
|
There was a problem hiding this comment.
The stream_created variable and its associated logic appear redundant. The variable is initialized to false, set to true in the error handler if the stream already exists, and then unconditionally set to true afterwards (lines 89-91) regardless of whether the CREATE succeeded or failed. Since the variable isn't used for any decision-making or validation, this logic can be removed entirely. The error handling for CREATE already properly handles the StreamAlreadyExists case.
| var stream_created = false; | |
| js.CREATE(&stream_config) catch |err| { | |
| if (err == error.StreamAlreadyExists) { | |
| std.debug.print("Token auth (JetStream): Stream already exists (OK)\n", .{}); | |
| stream_created = true; | |
| } else { | |
| std.debug.print("Token auth (JetStream): CREATE failed with error: {}\n", .{err}); | |
| return err; | |
| } | |
| }; | |
| if (!stream_created) { | |
| stream_created = true; | |
| } | |
| js.CREATE(&stream_config) catch |err| { | |
| if (err == error.StreamAlreadyExists) { | |
| std.debug.print("Token auth (JetStream): Stream already exists (OK)\n", .{}); | |
| } else { | |
| std.debug.print("Token auth (JetStream): CREATE failed with error: {}\n", .{err}); | |
| return err; | |
| } | |
| }; |
| var stream_created = false; | ||
| js.CREATE(&stream_config) catch |err| { | ||
| if (err == error.StreamAlreadyExists) { | ||
| std.debug.print("User/Pass auth (JetStream): Stream already exists (OK)\n", .{}); | ||
| stream_created = true; | ||
| } else { | ||
| std.debug.print("User/Pass auth (JetStream): CREATE failed with error: {}\n", .{err}); | ||
| return err; | ||
| } | ||
| }; | ||
|
|
||
| if (!stream_created) { | ||
| stream_created = true; | ||
| } | ||
|
|
There was a problem hiding this comment.
The stream_created variable and its associated logic appear redundant. The variable is initialized to false, set to true in the error handler if the stream already exists, and then unconditionally set to true afterwards (lines 163-165) regardless of whether the CREATE succeeded or failed. Since the variable isn't used for any decision-making or validation, this logic can be removed entirely. The error handling for CREATE already properly handles the StreamAlreadyExists case.
| var stream_created = false; | |
| js.CREATE(&stream_config) catch |err| { | |
| if (err == error.StreamAlreadyExists) { | |
| std.debug.print("User/Pass auth (JetStream): Stream already exists (OK)\n", .{}); | |
| stream_created = true; | |
| } else { | |
| std.debug.print("User/Pass auth (JetStream): CREATE failed with error: {}\n", .{err}); | |
| return err; | |
| } | |
| }; | |
| if (!stream_created) { | |
| stream_created = true; | |
| } | |
| js.CREATE(&stream_config) catch |err| { | |
| if (err == error.StreamAlreadyExists) { | |
| std.debug.print("User/Pass auth (JetStream): Stream already exists (OK)\n", .{}); | |
| } else { | |
| std.debug.print("User/Pass auth (JetStream): CREATE failed with error: {}\n", .{err}); | |
| return err; | |
| } | |
| }; |
| var stream_created = false; | ||
| js.CREATE(&stream_config) catch |err| { | ||
| // If stream already exists, that's fine - we can still test INFO | ||
| if (err == error.StreamAlreadyExists) { | ||
| std.debug.print("NKey auth (JetStream): Stream already exists (OK)\n", .{}); | ||
| stream_created = true; | ||
| } else { | ||
| // For any other error, this is a real failure | ||
| std.debug.print("NKey auth (JetStream): CREATE failed with error: {}\n", .{err}); | ||
| return err; | ||
| } | ||
| }; | ||
|
|
||
| if (!stream_created) { | ||
| stream_created = true; | ||
| } |
There was a problem hiding this comment.
The stream_created variable and its associated logic appear redundant. The variable is initialized to false, set to true in the error handler if the stream already exists, and then unconditionally set to true afterwards (lines 243-245) regardless of whether the CREATE succeeded or failed. Since the variable isn't used for any decision-making or validation, this logic can be removed entirely. The error handling for CREATE already properly handles the StreamAlreadyExists case.
| // Test 2: JetStream operations | ||
| { | ||
| const JetStream = @import("JetStream.zig"); | ||
|
|
||
| const js_opts: protocol.ConnectOpts = .{ | ||
| .addr = "127.0.0.1", | ||
| .port = 4223, | ||
| .auth_token = token, | ||
| }; | ||
|
|
||
| var js = try JetStream.CONNECT(allocator, js_opts); | ||
| defer js.DISCONNECT(); | ||
|
|
||
| const test_stream_name = "TOKEN_AUTH_TEST"; | ||
| var stream_config: protocol.StreamConfig = .{ | ||
| .name = test_stream_name, | ||
| .subjects = &.{"token.test.>"}, | ||
| }; | ||
|
|
||
| var stream_created = false; | ||
| js.CREATE(&stream_config) catch |err| { | ||
| if (err == error.StreamAlreadyExists) { | ||
| std.debug.print("Token auth (JetStream): Stream already exists (OK)\n", .{}); | ||
| stream_created = true; | ||
| } else { | ||
| std.debug.print("Token auth (JetStream): CREATE failed with error: {}\n", .{err}); | ||
| return err; | ||
| } | ||
| }; | ||
|
|
||
| if (!stream_created) { | ||
| stream_created = true; | ||
| } | ||
|
|
||
| const empty_request: JetStream.StreamInfoRequest = .{}; | ||
| const info = try js.INFO(test_stream_name, &empty_request); | ||
|
|
||
| std.debug.print("Token auth (JetStream): ✅ Stream INFO succeeded - {d} messages\n", .{info.state.?.messages}); | ||
|
|
||
| js.DELETE(test_stream_name) catch |err| { | ||
| std.debug.print("Token auth (JetStream): DELETE failed (OK): {}\n", .{err}); | ||
| }; | ||
| } |
There was a problem hiding this comment.
The JetStream test logic is duplicated across three authentication tests (token, user/pass, and nkey). Consider extracting this common test logic into a helper function that accepts the connection options as a parameter. This would reduce duplication and make the tests easier to maintain. The only differences between these blocks are the connection options and the stream/subject names used.
Addresses #17
Added JetStream tests in integration_test.zig.