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:
@@ -0,0 +1,117 @@
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Imports;
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Tests.Auth;
|
||||
|
||||
public class AccountResponseAndInterestParityBatch1Tests
|
||||
{
|
||||
[Fact]
|
||||
public void ClientInfoHdr_constant_matches_go_value()
|
||||
{
|
||||
Account.ClientInfoHdr.ShouldBe("Nats-Request-Info");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Interest_and_subscription_interest_count_plain_and_queue_matches()
|
||||
{
|
||||
using var account = new Account("A");
|
||||
account.SubList.Insert(new Subscription { Subject = "orders.*", Sid = "1" });
|
||||
account.SubList.Insert(new Subscription { Subject = "orders.*", Sid = "2", Queue = "workers" });
|
||||
|
||||
account.Interest("orders.created").ShouldBe(2);
|
||||
account.SubscriptionInterest("orders.created").ShouldBeTrue();
|
||||
account.SubscriptionInterest("payments.created").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumServiceImports_counts_distinct_from_subject_keys()
|
||||
{
|
||||
using var importer = new Account("importer");
|
||||
using var exporter = new Account("exporter");
|
||||
|
||||
importer.Imports.AddServiceImport(new ServiceImport
|
||||
{
|
||||
DestinationAccount = exporter,
|
||||
From = "svc.a",
|
||||
To = "svc.remote.a",
|
||||
});
|
||||
importer.Imports.AddServiceImport(new ServiceImport
|
||||
{
|
||||
DestinationAccount = exporter,
|
||||
From = "svc.a",
|
||||
To = "svc.remote.b",
|
||||
});
|
||||
importer.Imports.AddServiceImport(new ServiceImport
|
||||
{
|
||||
DestinationAccount = exporter,
|
||||
From = "svc.b",
|
||||
To = "svc.remote.c",
|
||||
});
|
||||
|
||||
importer.NumServiceImports().ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumPendingResponses_filters_by_service_export()
|
||||
{
|
||||
using var account = new Account("A");
|
||||
account.AddServiceExport("svc.one", ServiceResponseType.Singleton, null);
|
||||
account.AddServiceExport("svc.two", ServiceResponseType.Singleton, null);
|
||||
|
||||
var seOne = account.Exports.Services["svc.one"];
|
||||
var seTwo = account.Exports.Services["svc.two"];
|
||||
|
||||
account.Exports.Responses["r1"] = new ServiceImport
|
||||
{
|
||||
DestinationAccount = account,
|
||||
From = "_R_.AAA.>",
|
||||
To = "reply.one",
|
||||
Export = seOne,
|
||||
IsResponse = true,
|
||||
};
|
||||
account.Exports.Responses["r2"] = new ServiceImport
|
||||
{
|
||||
DestinationAccount = account,
|
||||
From = "_R_.BBB.>",
|
||||
To = "reply.two",
|
||||
Export = seOne,
|
||||
IsResponse = true,
|
||||
};
|
||||
account.Exports.Responses["r3"] = new ServiceImport
|
||||
{
|
||||
DestinationAccount = account,
|
||||
From = "_R_.CCC.>",
|
||||
To = "reply.three",
|
||||
Export = seTwo,
|
||||
IsResponse = true,
|
||||
};
|
||||
|
||||
account.NumPendingAllResponses().ShouldBe(3);
|
||||
account.NumPendingResponses("svc.one").ShouldBe(2);
|
||||
account.NumPendingResponses("svc.two").ShouldBe(1);
|
||||
account.NumPendingResponses("svc.unknown").ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveRespServiceImport_removes_mapping_for_specified_reason()
|
||||
{
|
||||
using var account = new Account("A");
|
||||
account.AddServiceExport("svc.one", ServiceResponseType.Singleton, null);
|
||||
var seOne = account.Exports.Services["svc.one"];
|
||||
|
||||
var responseSi = new ServiceImport
|
||||
{
|
||||
DestinationAccount = account,
|
||||
From = "_R_.ZZZ.>",
|
||||
To = "reply",
|
||||
Export = seOne,
|
||||
IsResponse = true,
|
||||
};
|
||||
account.Exports.Responses["r1"] = responseSi;
|
||||
|
||||
account.RemoveRespServiceImport(responseSi, ResponseServiceImportRemovalReason.Timeout);
|
||||
|
||||
account.Exports.Responses.Count.ShouldBe(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using NATS.Server.Auth;
|
||||
|
||||
namespace NATS.Server.Tests.Auth;
|
||||
|
||||
public class AuthModelAndCalloutConstantsParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void NkeyUser_exposes_parity_fields()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var nkeyUser = new NKeyUser
|
||||
{
|
||||
Nkey = "UABC",
|
||||
Issued = now,
|
||||
AllowedConnectionTypes = new HashSet<string> { "STANDARD", "WEBSOCKET" },
|
||||
ProxyRequired = true,
|
||||
};
|
||||
|
||||
nkeyUser.Issued.ShouldBe(now);
|
||||
nkeyUser.ProxyRequired.ShouldBeTrue();
|
||||
nkeyUser.AllowedConnectionTypes.ShouldContain("STANDARD");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void User_exposes_parity_fields()
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Username = "alice",
|
||||
Password = "secret",
|
||||
AllowedConnectionTypes = new HashSet<string> { "STANDARD" },
|
||||
ProxyRequired = false,
|
||||
};
|
||||
|
||||
user.ProxyRequired.ShouldBeFalse();
|
||||
user.AllowedConnectionTypes.ShouldContain("STANDARD");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void External_auth_callout_constants_match_go_subjects_and_header()
|
||||
{
|
||||
ExternalAuthCalloutAuthenticator.AuthCalloutSubject.ShouldBe("$SYS.REQ.USER.AUTH");
|
||||
ExternalAuthCalloutAuthenticator.AuthRequestSubject.ShouldBe("nats-authorization-request");
|
||||
ExternalAuthCalloutAuthenticator.AuthRequestXKeyHeader.ShouldBe("Nats-Server-Xkey");
|
||||
}
|
||||
}
|
||||
89
tests/NATS.Server.Tests/Auth/AuthServiceParityBatch4Tests.cs
Normal file
89
tests/NATS.Server.Tests/Auth/AuthServiceParityBatch4Tests.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using NATS.NKeys;
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Tests.Auth;
|
||||
|
||||
public class AuthServiceParityBatch4Tests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_assigns_global_account_to_orphan_users()
|
||||
{
|
||||
var service = AuthService.Build(new NatsOptions
|
||||
{
|
||||
Users = [new User { Username = "alice", Password = "secret" }],
|
||||
});
|
||||
|
||||
var result = service.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { Username = "alice", Password = "secret" },
|
||||
Nonce = [],
|
||||
});
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.AccountName.ShouldBe(Account.GlobalAccountName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_assigns_global_account_to_orphan_nkeys()
|
||||
{
|
||||
using var kp = KeyPair.CreatePair(PrefixByte.User);
|
||||
var pub = kp.GetPublicKey();
|
||||
var nonce = "test-nonce"u8.ToArray();
|
||||
var sig = new byte[64];
|
||||
kp.Sign(nonce, sig);
|
||||
|
||||
var service = AuthService.Build(new NatsOptions
|
||||
{
|
||||
NKeys = [new NKeyUser { Nkey = pub }],
|
||||
});
|
||||
|
||||
var result = service.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions
|
||||
{
|
||||
Nkey = pub,
|
||||
Sig = Convert.ToBase64String(sig),
|
||||
},
|
||||
Nonce = nonce,
|
||||
});
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.AccountName.ShouldBe(Account.GlobalAccountName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_validates_response_permissions_defaults_and_publish_allow()
|
||||
{
|
||||
var service = AuthService.Build(new NatsOptions
|
||||
{
|
||||
Users =
|
||||
[
|
||||
new User
|
||||
{
|
||||
Username = "alice",
|
||||
Password = "secret",
|
||||
Permissions = new Permissions
|
||||
{
|
||||
Response = new ResponsePermission { MaxMsgs = 0, Expires = TimeSpan.Zero },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
var result = service.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { Username = "alice", Password = "secret" },
|
||||
Nonce = [],
|
||||
});
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Permissions.ShouldNotBeNull();
|
||||
result.Permissions.Response.ShouldNotBeNull();
|
||||
result.Permissions.Response.MaxMsgs.ShouldBe(NatsProtocol.DefaultAllowResponseMaxMsgs);
|
||||
result.Permissions.Response.Expires.ShouldBe(NatsProtocol.DefaultAllowResponseExpiration);
|
||||
result.Permissions.Publish.ShouldNotBeNull();
|
||||
result.Permissions.Publish.Allow.ShouldNotBeNull();
|
||||
result.Permissions.Publish.Allow.Count.ShouldBe(0);
|
||||
}
|
||||
}
|
||||
65
tests/NATS.Server.Tests/Auth/TlsMapAuthParityBatch1Tests.cs
Normal file
65
tests/NATS.Server.Tests/Auth/TlsMapAuthParityBatch1Tests.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using NATS.Server.Auth;
|
||||
|
||||
namespace NATS.Server.Tests.Auth;
|
||||
|
||||
public class TlsMapAuthParityBatch1Tests
|
||||
{
|
||||
[Fact]
|
||||
public void GetTlsAuthDcs_extracts_domain_components_from_subject()
|
||||
{
|
||||
using var cert = CreateSelfSignedCert("CN=alice,DC=example,DC=com");
|
||||
|
||||
TlsMapAuthenticator.GetTlsAuthDcs(cert.SubjectName).ShouldBe("DC=example,DC=com");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DnsAltNameLabels_and_matches_follow_rfc6125_shape()
|
||||
{
|
||||
var labels = TlsMapAuthenticator.DnsAltNameLabels("*.Example.COM");
|
||||
labels.ShouldBe(["*", "example", "com"]);
|
||||
|
||||
TlsMapAuthenticator.DnsAltNameMatches(labels, [new Uri("nats://node.example.com:6222")]).ShouldBeTrue();
|
||||
TlsMapAuthenticator.DnsAltNameMatches(labels, [new Uri("nats://a.b.example.com:6222")]).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Authenticate_can_match_user_from_email_or_dns_san()
|
||||
{
|
||||
using var cert = CreateSelfSignedCertWithSan("CN=ignored", "ops@example.com", "router.example.com");
|
||||
var auth = new TlsMapAuthenticator([
|
||||
new User { Username = "ops@example.com", Password = "" },
|
||||
new User { Username = "router.example.com", Password = "" },
|
||||
]);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new Protocol.ClientOptions(),
|
||||
Nonce = [],
|
||||
ClientCertificate = cert,
|
||||
};
|
||||
|
||||
var result = auth.Authenticate(ctx);
|
||||
result.ShouldNotBeNull();
|
||||
(result.Identity == "ops@example.com" || result.Identity == "router.example.com").ShouldBeTrue();
|
||||
}
|
||||
|
||||
private static X509Certificate2 CreateSelfSignedCert(string subjectName)
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var req = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
return req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1));
|
||||
}
|
||||
|
||||
private static X509Certificate2 CreateSelfSignedCertWithSan(string subjectName, string email, string dns)
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var req = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
var sans = new SubjectAlternativeNameBuilder();
|
||||
sans.AddEmailAddress(email);
|
||||
sans.AddDnsName(dns);
|
||||
req.CertificateExtensions.Add(sans.Build());
|
||||
return req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user