Fix E2E test gaps and add comprehensive E2E + parity test suites
- Fix pull consumer fetch: send original stream subject in HMSG (not inbox) so NATS client distinguishes data messages from control messages - Fix MaxAge expiry: add background timer in StreamManager for periodic pruning - Fix JetStream wire format: Go-compatible anonymous objects with string enums, proper offset-based pagination for stream/consumer list APIs - Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream) - Add ~1000 parity tests across all subsystems (gaps closure) - Update gap inventory docs to reflect implementation status
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Shouldly;
|
||||
using NATS.Server.WebSocket;
|
||||
|
||||
namespace NATS.Server.Tests.WebSocket;
|
||||
|
||||
@@ -23,4 +24,27 @@ public class WebSocketOptionsTests
|
||||
opts.WebSocket.ShouldNotBeNull();
|
||||
opts.WebSocket.Port.ShouldBe(-1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WsAuthConfig_sets_auth_override_when_websocket_auth_fields_are_present()
|
||||
{
|
||||
var ws = new WebSocketOptions
|
||||
{
|
||||
Username = "u",
|
||||
};
|
||||
|
||||
WsAuthConfig.Apply(ws);
|
||||
|
||||
ws.AuthOverride.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WsAuthConfig_keeps_auth_override_false_when_no_ws_auth_fields_are_present()
|
||||
{
|
||||
var ws = new WebSocketOptions();
|
||||
|
||||
WsAuthConfig.Apply(ws);
|
||||
|
||||
ws.AuthOverride.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.WebSocket;
|
||||
|
||||
namespace NATS.Server.Tests.WebSocket;
|
||||
|
||||
public class WebSocketOptionsValidatorParityBatch2Tests
|
||||
{
|
||||
[Fact]
|
||||
public void Validate_rejects_tls_listener_without_cert_key_when_not_no_tls()
|
||||
{
|
||||
var opts = new NatsOptions
|
||||
{
|
||||
WebSocket = new WebSocketOptions
|
||||
{
|
||||
Port = 8080,
|
||||
NoTls = false,
|
||||
},
|
||||
};
|
||||
|
||||
var result = WebSocketOptionsValidator.Validate(opts);
|
||||
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain(e => e.Contains("TLS", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_rejects_invalid_allowed_origins()
|
||||
{
|
||||
var opts = new NatsOptions
|
||||
{
|
||||
WebSocket = new WebSocketOptions
|
||||
{
|
||||
Port = 8080,
|
||||
NoTls = true,
|
||||
AllowedOrigins = ["not-a-uri"],
|
||||
},
|
||||
};
|
||||
|
||||
var result = WebSocketOptionsValidator.Validate(opts);
|
||||
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain(e => e.Contains("allowed origin", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_rejects_no_auth_user_not_present_in_configured_users()
|
||||
{
|
||||
var opts = new NatsOptions
|
||||
{
|
||||
Users = [new User { Username = "alice", Password = "x" }],
|
||||
WebSocket = new WebSocketOptions
|
||||
{
|
||||
Port = 8080,
|
||||
NoTls = true,
|
||||
NoAuthUser = "bob",
|
||||
},
|
||||
};
|
||||
|
||||
var result = WebSocketOptionsValidator.Validate(opts);
|
||||
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain(e => e.Contains("NoAuthUser", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_rejects_username_or_token_when_users_or_nkeys_are_set()
|
||||
{
|
||||
var opts = new NatsOptions
|
||||
{
|
||||
Users = [new User { Username = "alice", Password = "x" }],
|
||||
WebSocket = new WebSocketOptions
|
||||
{
|
||||
Port = 8080,
|
||||
NoTls = true,
|
||||
Username = "ws-user",
|
||||
},
|
||||
};
|
||||
|
||||
var result = WebSocketOptionsValidator.Validate(opts);
|
||||
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain(e => e.Contains("users", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_rejects_jwt_cookie_without_trusted_operators()
|
||||
{
|
||||
var opts = new NatsOptions
|
||||
{
|
||||
WebSocket = new WebSocketOptions
|
||||
{
|
||||
Port = 8080,
|
||||
NoTls = true,
|
||||
JwtCookie = "jwt",
|
||||
},
|
||||
};
|
||||
|
||||
var result = WebSocketOptionsValidator.Validate(opts);
|
||||
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain(e => e.Contains("JwtCookie", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_rejects_reserved_response_headers_override()
|
||||
{
|
||||
var opts = new NatsOptions
|
||||
{
|
||||
TrustedKeys = ["OP1"],
|
||||
WebSocket = new WebSocketOptions
|
||||
{
|
||||
Port = 8080,
|
||||
NoTls = true,
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
["Sec-WebSocket-Accept"] = "bad",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var result = WebSocketOptionsValidator.Validate(opts);
|
||||
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain(e => e.Contains("reserved", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_rejects_tls_pinned_certs_when_websocket_tls_is_disabled()
|
||||
{
|
||||
var opts = new NatsOptions
|
||||
{
|
||||
TlsPinnedCerts = ["ABCDEF0123"],
|
||||
WebSocket = new WebSocketOptions
|
||||
{
|
||||
Port = 8080,
|
||||
NoTls = true,
|
||||
},
|
||||
};
|
||||
|
||||
var result = WebSocketOptionsValidator.Validate(opts);
|
||||
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain(e => e.Contains("TLSPinnedCerts", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_accepts_valid_minimal_configuration()
|
||||
{
|
||||
var opts = new NatsOptions
|
||||
{
|
||||
TrustedKeys = ["OP1"],
|
||||
Users = [new User { Username = "alice", Password = "x" }],
|
||||
WebSocket = new WebSocketOptions
|
||||
{
|
||||
Port = 8080,
|
||||
NoTls = true,
|
||||
NoAuthUser = "alice",
|
||||
AllowedOrigins = ["https://app.example.com"],
|
||||
JwtCookie = "jwt",
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
["X-App-Version"] = "1",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var result = WebSocketOptionsValidator.Validate(opts);
|
||||
|
||||
result.IsValid.ShouldBeTrue();
|
||||
result.Errors.ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Text;
|
||||
using NATS.Server.WebSocket;
|
||||
|
||||
namespace NATS.Server.Tests.WebSocket;
|
||||
|
||||
public class WsUpgradeHelperParityBatch1Tests
|
||||
{
|
||||
[Fact]
|
||||
public void MakeChallengeKey_returns_base64_of_16_random_bytes()
|
||||
{
|
||||
var key = WsUpgrade.MakeChallengeKey();
|
||||
var decoded = Convert.FromBase64String(key);
|
||||
|
||||
decoded.Length.ShouldBe(16);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Url_helpers_match_ws_and_wss_schemes()
|
||||
{
|
||||
WsUpgrade.IsWsUrl("ws://localhost:8080").ShouldBeTrue();
|
||||
WsUpgrade.IsWsUrl("wss://localhost:8443").ShouldBeFalse();
|
||||
WsUpgrade.IsWsUrl("http://localhost").ShouldBeFalse();
|
||||
|
||||
WsUpgrade.IsWssUrl("wss://localhost:8443").ShouldBeTrue();
|
||||
WsUpgrade.IsWssUrl("ws://localhost:8080").ShouldBeFalse();
|
||||
WsUpgrade.IsWssUrl("https://localhost").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RejectNoMaskingForTest_forces_no_masking_handshake_rejection()
|
||||
{
|
||||
var request = BuildValidRequest("/leafnode", "Nats-No-Masking: true\r\n");
|
||||
using var input = new MemoryStream(Encoding.ASCII.GetBytes(request));
|
||||
using var output = new MemoryStream();
|
||||
|
||||
try
|
||||
{
|
||||
WsUpgrade.RejectNoMaskingForTest = true;
|
||||
var result = await WsUpgrade.TryUpgradeAsync(input, output, new WebSocketOptions { NoTls = true });
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
output.Position = 0;
|
||||
var response = Encoding.ASCII.GetString(output.ToArray());
|
||||
response.ShouldContain("400 Bad Request");
|
||||
response.ShouldContain("invalid value for no-masking");
|
||||
}
|
||||
finally
|
||||
{
|
||||
WsUpgrade.RejectNoMaskingForTest = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildValidRequest(string path = "/", string extraHeaders = "")
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"GET {path} HTTP/1.1\r\n");
|
||||
sb.Append("Host: localhost:8080\r\n");
|
||||
sb.Append("Upgrade: websocket\r\n");
|
||||
sb.Append("Connection: Upgrade\r\n");
|
||||
sb.Append("Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n");
|
||||
sb.Append("Sec-WebSocket-Version: 13\r\n");
|
||||
sb.Append(extraHeaders);
|
||||
sb.Append("\r\n");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user