using System.Text; using System.Text.Json; using NATS.Client.Core; using NATS.E2E.Tests.Infrastructure; using NATS.NKeys; namespace NATS.E2E.Tests; public class AdvancedTests { [Fact] public async Task ConfigFile_FullConfig_ServerStartsAndAcceptsConnections() { var config = """ server_name: e2e-config-test max_payload: 2048 max_connections: 100 """; await using var server = NatsServerProcess.WithConfig(config); await server.StartAsync(); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{server.Port}" }); await client.ConnectAsync(); await client.PingAsync(); client.ConnectionState.ShouldBe(NatsConnectionState.Open); client.ServerInfo.ShouldNotBeNull(); client.ServerInfo!.MaxPayload.ShouldBe(2048); } [Fact] public async Task MaxConnections_ExceedsLimit_Rejected() { var config = """ max_connections: 2 """; await using var server = NatsServerProcess.WithConfig(config); await server.StartAsync(); var url = $"nats://127.0.0.1:{server.Port}"; await using var c1 = new NatsConnection(new NatsOpts { Url = url }); await using var c2 = new NatsConnection(new NatsOpts { Url = url }); await c1.ConnectAsync(); await c1.PingAsync(); await c2.ConnectAsync(); await c2.PingAsync(); await using var c3 = new NatsConnection(new NatsOpts { Url = url, MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await c3.ConnectAsync(); await c3.PingAsync(); }); ex.ShouldNotBeNull(); } [Fact] public async Task SystemEvents_ClientConnect_EventPublished() { var config = """ accounts { SYS { users = [{ user: "sys", password: "sys" }] } APP { users = [{ user: "app", password: "app" }] } } system_account: SYS """; await using var server = NatsServerProcess.WithConfig(config); await server.StartAsync(); var url = $"nats://127.0.0.1:{server.Port}"; await using var sysClient = new NatsConnection(new NatsOpts { Url = url, AuthOpts = new NatsAuthOpts { Username = "sys", Password = "sys" }, }); await sysClient.ConnectAsync(); await using var subscription = await sysClient.SubscribeCoreAsync("$SYS.ACCOUNT.*.CONNECT"); await sysClient.PingAsync(); await using var appClient = new NatsConnection(new NatsOpts { Url = url, AuthOpts = new NatsAuthOpts { Username = "app", Password = "app" }, }); await appClient.ConnectAsync(); await appClient.PingAsync(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var msg = await subscription.Msgs.ReadAsync(cts.Token); msg.Subject.ShouldContain("CONNECT"); } [Fact] public async Task AccountImportExport_CrossAccountServiceCall() { var config = """ accounts { PROVIDER { users = [{ user: "provider", password: "prov" }] exports = [ { service: "svc.echo" } ] } CONSUMER { users = [{ user: "consumer", password: "cons" }] imports = [ { service: { account: PROVIDER, subject: "svc.echo" } } ] } } """; await using var server = NatsServerProcess.WithConfig(config); await server.StartAsync(); var url = $"nats://127.0.0.1:{server.Port}"; await using var provider = new NatsConnection(new NatsOpts { Url = url, AuthOpts = new NatsAuthOpts { Username = "provider", Password = "prov" }, }); await provider.ConnectAsync(); await using var svcSub = await provider.SubscribeCoreAsync("svc.echo"); await provider.PingAsync(); var responderTask = Task.Run(async () => { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var msg = await svcSub.Msgs.ReadAsync(cts.Token); await provider.PublishAsync(msg.ReplyTo!, $"echo: {msg.Data}"); }); await using var consumer = new NatsConnection(new NatsOpts { Url = url, AuthOpts = new NatsAuthOpts { Username = "consumer", Password = "cons" }, }); await consumer.ConnectAsync(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var reply = await consumer.RequestAsync("svc.echo", "hello", cancellationToken: cts.Token); reply.Data.ShouldBe("echo: hello"); await responderTask; } [Fact] public async Task ServiceLatency_CrossAccountCall_LatencyMessagePublished() { var config = """ accounts { SYS { users = [{ user: "sys", password: "sys" }] } PROVIDER { users = [{ user: "provider", password: "prov" }] exports = [ { service: "svc.echo", latency: "latency.svc.echo" } ] } CONSUMER { users = [{ user: "consumer", password: "cons" }] imports = [ { service: { account: PROVIDER, subject: "svc.echo" } } ] } } system_account: SYS """; await using var server = NatsServerProcess.WithConfig(config); await server.StartAsync(); var url = $"nats://127.0.0.1:{server.Port}"; // System account client subscribes to latency events await using var sysClient = new NatsConnection(new NatsOpts { Url = url, AuthOpts = new NatsAuthOpts { Username = "sys", Password = "sys" }, }); await sysClient.ConnectAsync(); await using var latencySub = await sysClient.SubscribeCoreAsync("latency.svc.echo"); await sysClient.PingAsync(); // Provider sets up the echo responder await using var provider = new NatsConnection(new NatsOpts { Url = url, AuthOpts = new NatsAuthOpts { Username = "provider", Password = "prov" }, }); await provider.ConnectAsync(); await using var svcSub = await provider.SubscribeCoreAsync("svc.echo"); await provider.PingAsync(); var responderTask = Task.Run(async () => { using var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var msg = await svcSub.Msgs.ReadAsync(cts2.Token); await provider.PublishAsync(msg.ReplyTo!, $"echo: {msg.Data}"); }); // Consumer makes a cross-account service call await using var consumer = new NatsConnection(new NatsOpts { Url = url, AuthOpts = new NatsAuthOpts { Username = "consumer", Password = "cons" }, }); await consumer.ConnectAsync(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var reply = await consumer.RequestAsync("svc.echo", "hello", cancellationToken: cts.Token); reply.Data.ShouldBe("echo: hello"); await responderTask; // Verify latency message was published to the system account var latencyMsg = await latencySub.Msgs.ReadAsync(cts.Token); latencyMsg.Subject.ShouldBe("latency.svc.echo"); latencyMsg.Data.ShouldNotBeNull(); latencyMsg.Data!.ShouldContain("service_latency"); } [Fact] public async Task SubjectTransforms_MappedSubject_ReceivedOnTarget() { var config = """ mappings { "e2e.src": "e2e.dest" } """; await using var server = NatsServerProcess.WithConfig(config); await server.StartAsync(); var url = $"nats://127.0.0.1:{server.Port}"; await using var client = new NatsConnection(new NatsOpts { Url = url }); await client.ConnectAsync(); // Subscribe to the destination subject await using var sub = await client.SubscribeCoreAsync("e2e.dest"); await client.PingAsync(); // Publish to the source subject — should be transformed to destination using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await client.PublishAsync("e2e.src", "mapped-payload", cancellationToken: cts.Token); await client.PingAsync(cts.Token); var msg = await sub.Msgs.ReadAsync(cts.Token); msg.Data.ShouldBe("mapped-payload"); } [Fact] public async Task JwtAuth_ValidJwt_Connects() { // Generate operator, account, and user NKey pairs using var operatorKp = KeyPair.CreatePair(PrefixByte.Operator); using var accountKp = KeyPair.CreatePair(PrefixByte.Account); using var userKp = KeyPair.CreatePair(PrefixByte.User); var operatorPub = operatorKp.GetPublicKey(); var accountPub = accountKp.GetPublicKey(); var userPub = userKp.GetPublicKey(); // Build account JWT (signed by operator) var accountJwt = BuildJwt(new { sub = accountPub, iss = operatorPub, iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), nats = new { type = "account", version = 2 }, }, operatorKp); // Build user JWT as bearer token (signed by account, no nonce needed) var userJwt = BuildJwt(new { sub = userPub, iss = accountPub, iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), nats = new { type = "user", version = 2, bearer_token = true, issuer_account = accountPub }, }, accountKp); var config = $$""" trusted_keys: "{{operatorPub}}" resolver: MEMORY resolver_preload: { {{accountPub}}: "{{accountJwt}}" } """; await using var server = NatsServerProcess.WithConfig(config); await server.StartAsync(); var url = $"nats://127.0.0.1:{server.Port}"; await using var client = new NatsConnection(new NatsOpts { Url = url, AuthOpts = new NatsAuthOpts { Jwt = userJwt }, }); await client.ConnectAsync(); await client.PingAsync(); client.ConnectionState.ShouldBe(NatsConnectionState.Open); } /// /// Builds a signed NATS JWT using the given payload and NKey pair. /// Wire format: base64url(header).base64url(payload).base64url(ed25519-signature). /// private static string BuildJwt(object payload, KeyPair signingKp) { var header = """{"typ":"jwt","alg":"ed25519-nkey"}"""; var payloadJson = JsonSerializer.Serialize(payload); var headerB64 = Base64UrlEncode(Encoding.UTF8.GetBytes(header)); var payloadB64 = Base64UrlEncode(Encoding.UTF8.GetBytes(payloadJson)); var signingInput = Encoding.UTF8.GetBytes($"{headerB64}.{payloadB64}"); var sig = new byte[64]; signingKp.Sign(signingInput, sig); return $"{headerB64}.{payloadB64}.{Base64UrlEncode(sig)}"; } private static string Base64UrlEncode(byte[] data) => Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_'); }