diff --git a/tests/NATS.E2E.Tests/AdvancedTests.cs b/tests/NATS.E2E.Tests/AdvancedTests.cs new file mode 100644 index 0000000..42634c3 --- /dev/null +++ b/tests/NATS.E2E.Tests/AdvancedTests.cs @@ -0,0 +1,180 @@ +using NATS.Client.Core; +using NATS.E2E.Tests.Infrastructure; + +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(Skip = "system_account promotion not yet wired: events route through internal $SYS account, not user-defined account")] + [SlopwatchSuppress("SW001", "User-defined system_account SubList is not bridged to the internal event system; tracked as a future milestone")] + 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(Skip = "Cross-account service routing not yet implemented in message dispatch path")] + [SlopwatchSuppress("SW001", "Cross-account service import routing is not yet wired in the message dispatch path; tracked as a future milestone")] + 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(Skip = "Subject transforms not yet implemented in config parsing")] + [SlopwatchSuppress("SW001", "Subject transform config parsing is not yet implemented; tracked for future milestone")] + public Task SubjectTransforms_MappedSubject_ReceivedOnTarget() + { + return Task.CompletedTask; + } + + [Fact(Skip = "JWT operator mode not yet implemented in config parsing")] + [SlopwatchSuppress("SW001", "JWT operator mode config parsing is not yet implemented; tracked for future milestone")] + public Task JwtAuth_ValidJwt_Connects() + { + return Task.CompletedTask; + } +}