feat: wire remaining E2E gaps — account imports, subject transforms, JWT auth, service latency

Close all 5 server-side wiring gaps so E2E tests pass without skips:
- System events: bridge user-defined system_account to internal $SYS
- Account imports/exports: config parsing + reverse response import for cross-account request-reply
- Subject transforms: parse mappings config block, apply in ProcessMessage
- JWT auth: parse trusted_keys, resolver MEMORY, resolver_preload in config
- Service latency: timestamp on request, publish ServiceLatencyMsg on response
This commit is contained in:
Joseph Doherty
2026-03-12 23:03:12 -04:00
parent 246fc7ad87
commit 95e9f0a92e
5 changed files with 571 additions and 16 deletions

View File

@@ -1,5 +1,8 @@
using System.Text;
using System.Text.Json;
using NATS.Client.Core;
using NATS.E2E.Tests.Infrastructure;
using NATS.NKeys;
namespace NATS.E2E.Tests;
@@ -62,8 +65,7 @@ public class AdvancedTests
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")]
[Fact]
public async Task SystemEvents_ClientConnect_EventPublished()
{
var config = """
@@ -106,8 +108,7 @@ public class AdvancedTests
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")]
[Fact]
public async Task AccountImportExport_CrossAccountServiceCall()
{
var config = """
@@ -164,17 +165,188 @@ public class AdvancedTests
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()
[Fact]
public async Task ServiceLatency_CrossAccountCall_LatencyMessagePublished()
{
return Task.CompletedTask;
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<string>("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<string>("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<string, string>("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(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()
[Fact]
public async Task SubjectTransforms_MappedSubject_ReceivedOnTarget()
{
return Task.CompletedTask;
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<string>("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);
}
/// <summary>
/// Builds a signed NATS JWT using the given payload and NKey pair.
/// Wire format: base64url(header).base64url(payload).base64url(ed25519-signature).
/// </summary>
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('/', '_');
}