using System.Text; using NATS.Server.JetStream; using NATS.Server.JetStream.Api; using NATS.Server.JetStream.Consumers; using NATS.Server.JetStream.Models; using NATS.Server.JetStream.Publish; namespace NATS.Server.Tests; internal sealed class JetStreamApiFixture : IAsyncDisposable { private static readonly StreamManager SharedStreamManager = new(); private static readonly ConsumerManager SharedConsumerManager = new(); private static readonly JetStreamApiRouter SharedRouter = new(SharedStreamManager, SharedConsumerManager); private readonly StreamManager _streamManager; private readonly ConsumerManager _consumerManager; private readonly JetStreamApiRouter _router; private readonly JetStreamPublisher _publisher; private JetStreamApiFixture() { _streamManager = new StreamManager(); _consumerManager = new ConsumerManager(); _router = new JetStreamApiRouter(_streamManager, _consumerManager); _publisher = new JetStreamPublisher(_streamManager); } public static Task RequestAsync(string subject, string payload) { return Task.FromResult(SharedRouter.Route(subject, Encoding.UTF8.GetBytes(payload))); } public static async Task StartWithStreamAsync(string streamName, string subject, int maxMsgs = 0) { var fixture = new JetStreamApiFixture(); var payload = $"{{\"name\":\"{streamName}\",\"subjects\":[\"{subject}\"],\"max_msgs\":{maxMsgs}}}"; _ = await fixture.RequestLocalAsync($"$JS.API.STREAM.CREATE.{streamName}", payload); return fixture; } public static async Task StartWithPullConsumerAsync() { var fixture = await StartWithStreamAsync("ORDERS", "orders.*"); _ = await fixture.CreateConsumerAsync("ORDERS", "PULL", "orders.created"); return fixture; } public static async Task StartWithPushConsumerAsync() { var fixture = await StartWithStreamAsync("ORDERS", "orders.*"); _ = await fixture.CreateConsumerAsync("ORDERS", "PUSH", "orders.created", push: true, heartbeatMs: 25); return fixture; } public Task PublishAndGetAckAsync(string subject, string payload, string? msgId = null, bool expectError = false) { if (_publisher.TryCapture(subject, Encoding.UTF8.GetBytes(payload), msgId, out var ack)) { if (ack.ErrorCode == null && _streamManager.TryGet(ack.Stream, out var streamHandle)) { var stored = streamHandle.Store.LoadAsync(ack.Seq, default).GetAwaiter().GetResult(); if (stored != null) _consumerManager.OnPublished(ack.Stream, stored); } return Task.FromResult(ack); } if (expectError) return Task.FromResult(new PubAck { ErrorCode = 404 }); throw new InvalidOperationException($"No stream matched subject '{subject}'."); } public Task RequestLocalAsync(string subject, string payload) { return Task.FromResult(_router.Route(subject, Encoding.UTF8.GetBytes(payload))); } public Task GetStreamStateAsync(string streamName) { return _streamManager.GetStateAsync(streamName, default).AsTask(); } public Task CreateConsumerAsync(string stream, string durableName, string filterSubject, bool push = false, int heartbeatMs = 0) { var payload = $@"{{""durable_name"":""{durableName}"",""filter_subject"":""{filterSubject}"",""push"":{push.ToString().ToLowerInvariant()},""heartbeat_ms"":{heartbeatMs}}}"; return RequestLocalAsync($"$JS.API.CONSUMER.CREATE.{stream}.{durableName}", payload); } public async Task GetConsumerInfoAsync(string stream, string durableName) { var response = await RequestLocalAsync($"$JS.API.CONSUMER.INFO.{stream}.{durableName}", "{}"); return response.ConsumerInfo ?? throw new InvalidOperationException("Consumer not found."); } public Task FetchAsync(string stream, string durableName, int batch) { return _consumerManager.FetchAsync(stream, durableName, batch, _streamManager, default).AsTask(); } public Task ReadPushFrameAsync(string stream = "ORDERS", string durableName = "PUSH") { var frame = _consumerManager.ReadPushFrame(stream, durableName); if (frame == null) throw new InvalidOperationException("No push frame available."); return Task.FromResult(frame); } public ValueTask DisposeAsync() => ValueTask.CompletedTask; }