diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MessageBlock.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MessageBlock.cs index 32aef58..743b365 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MessageBlock.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MessageBlock.cs @@ -1585,6 +1585,44 @@ internal sealed class MessageBlock return null; } + // Close the message block. + internal void Close(bool sync) + { + Mu.EnterWriteLock(); + try + { + if (Closed) + return; + + if (Ctmr != null) + { + Ctmr.Dispose(); + Ctmr = null; + } + + Fss = null; + _ = FlushPendingMsgsLocked(); + ClearCacheAndOffset(); + + Qch?.Writer.TryComplete(); + Qch = null; + + if (Mfd != null) + { + if (sync) + Mfd.Flush(flushToDisk: true); + Mfd.Dispose(); + } + + Mfd = null; + Closed = true; + } + finally + { + Mu.ExitWriteLock(); + } + } + // Remove a sequence from per-subject state and track whether first/last need recompute. // Lock should be held. internal ulong RemoveSeqPerSubject(string subj, ulong seq) @@ -3300,9 +3338,125 @@ public sealed class ConsumerFileStore : IConsumerStore _name = name; _odir = odir; _ifn = Path.Combine(odir, FileStoreDefaults.ConsumerState); + _fch = Channel.CreateBounded(1); + _qch = Channel.CreateUnbounded(); + _ = Task.Run(() => FlushLoop(_fch, _qch)); + lock (_mu) { - TryLoadStateLocked(); + var loadErr = LoadState(); + if (loadErr != null) + throw loadErr; + } + } + + internal Exception? ConvertCipher() + { + byte[]? state; + Exception? err; + lock (_mu) + { + (state, err) = EncodeStateLocked(); + } + + if (err != null || state == null) + return err ?? new InvalidOperationException("unable to encode consumer state"); + + var metaErr = WriteConsumerMeta(); + if (metaErr != null) + return metaErr; + + return WriteState(state); + } + + // Kick flusher for this consumer. + // Lock should be held. + internal void KickFlusher() + { + if (_fch != null) + _ = _fch.Writer.TryWrite(0); + _dirty = true; + } + + // Set in flusher status. + internal void SetInFlusher() + { + lock (_mu) + { + _flusher = true; + } + } + + // Clear in flusher status. + internal void ClearInFlusher() + { + lock (_mu) + { + _flusher = false; + } + } + + // Report in flusher status. + internal bool InFlusher() + { + lock (_mu) + { + return _flusher; + } + } + + // flushLoop watches for consumer updates and the quit channel. + internal void FlushLoop(Channel? fch, Channel? qch) + { + SetInFlusher(); + try + { + if (fch == null) + return; + + var minTime = TimeSpan.FromMilliseconds(100); + var lastWrite = DateTime.MinValue; + + while (true) + { + if (qch?.Reader.Completion.IsCompleted == true) + return; + + var canRead = fch.Reader.WaitToReadAsync().AsTask().GetAwaiter().GetResult(); + if (!canRead) + return; + + while (fch.Reader.TryRead(out _)) + { + } + + if (lastWrite != DateTime.MinValue) + { + var elapsed = DateTime.UtcNow - lastWrite; + if (elapsed < minTime) + Thread.Sleep(minTime - elapsed); + } + + byte[]? buf; + Exception? encodeErr; + lock (_mu) + { + if (_closed) + return; + + (buf, encodeErr) = EncodeStateLocked(); + } + + if (encodeErr != null || buf == null) + return; + + if (WriteState(buf) == null) + lastWrite = DateTime.UtcNow; + } + } + finally + { + ClearInFlusher(); } } @@ -3313,12 +3467,21 @@ public sealed class ConsumerFileStore : IConsumerStore /// public void SetStarting(ulong sseq) { + byte[]? buf; + Exception? encodeErr; lock (_mu) { _state.Delivered.Stream = sseq; _state.AckFloor.Stream = sseq; - PersistStateLocked(); + (buf, encodeErr) = EncodeStateLocked(); } + + if (encodeErr != null || buf == null) + throw encodeErr ?? new InvalidOperationException("unable to encode consumer state"); + + var writeErr = WriteState(buf); + if (writeErr != null) + throw writeErr; } /// @@ -3332,7 +3495,7 @@ public sealed class ConsumerFileStore : IConsumerStore _state.Delivered.Stream = sseq; if (_cfg.Config.AckPolicy == AckPolicy.AckNone) _state.AckFloor.Stream = sseq; - PersistStateLocked(); + KickFlusher(); } } @@ -3342,10 +3505,9 @@ public sealed class ConsumerFileStore : IConsumerStore lock (_mu) { _state = new ConsumerState(); - _state.Delivered.Stream = sseq; - _state.AckFloor.Stream = sseq; - PersistStateLocked(); } + + SetStarting(sseq); } /// @@ -3353,10 +3515,10 @@ public sealed class ConsumerFileStore : IConsumerStore { lock (_mu) { - return _state.Delivered.Consumer != 0 || - _state.Delivered.Stream != 0 || - _state.Pending is { Count: > 0 } || - _state.Redelivered is { Count: > 0 }; + if (_state.Delivered.Consumer != 0 || _state.Delivered.Stream != 0) + return true; + + return File.Exists(_ifn); } } @@ -3418,7 +3580,7 @@ public sealed class ConsumerFileStore : IConsumerStore } } - PersistStateLocked(); + KickFlusher(); } } @@ -3471,7 +3633,7 @@ public sealed class ConsumerFileStore : IConsumerStore } } - PersistStateLocked(); + KickFlusher(); return; } @@ -3510,17 +3672,21 @@ public sealed class ConsumerFileStore : IConsumerStore } _state.Redelivered?.Remove(sseq); - PersistStateLocked(); + KickFlusher(); } } /// public void UpdateConfig(ConsumerConfig cfg) + => _ = UpdateConfigInternal(cfg); + + // Used to update config for recovered ephemerals. + internal Exception? UpdateConfigInternal(ConsumerConfig cfg) { lock (_mu) { _cfg.Config = cfg; - PersistStateLocked(); + return WriteConsumerMeta(); } } @@ -3544,31 +3710,17 @@ public sealed class ConsumerFileStore : IConsumerStore throw new InvalidOperationException("old update ignored"); _state = CloneState(state, copyCollections: true); - PersistStateLocked(); + KickFlusher(); } } /// public (ConsumerState? State, Exception? Error) State() - { - lock (_mu) - { - if (_closed) - return (null, StoreErrors.ErrStoreClosed); - return (CloneState(_state, copyCollections: true), null); - } - } + => StateWithCopy(doCopy: true); /// public (ConsumerState? State, Exception? Error) BorrowState() - { - lock (_mu) - { - if (_closed) - return (null, StoreErrors.ErrStoreClosed); - return (CloneState(_state, copyCollections: false), null); - } - } + => StateWithCopy(doCopy: false); /// public byte[] EncodedState() @@ -3577,7 +3729,10 @@ public sealed class ConsumerFileStore : IConsumerStore { if (_closed) throw StoreErrors.ErrStoreClosed; - return JsonSerializer.SerializeToUtf8Bytes(CloneState(_state, copyCollections: true)); + var (buf, err) = EncodeStateLocked(); + if (err != null || buf == null) + throw err ?? new InvalidOperationException("unable to encode consumer state"); + return buf; } } @@ -3587,27 +3742,257 @@ public sealed class ConsumerFileStore : IConsumerStore /// public void Stop() { + byte[]? buf = null; lock (_mu) { if (_closed) return; - PersistStateLocked(); + + _qch?.Writer.TryComplete(); + _qch = null; + + var hasState = _state.Delivered.Consumer != 0 || + _state.Delivered.Stream != 0 || + _state.Pending is { Count: > 0 } || + _state.Redelivered is { Count: > 0 }; + + if (_dirty || hasState) + { + var (encoded, err) = EncodeStateLocked(); + if (err == null) + buf = encoded; + } + _closed = true; } + _fs.RemoveConsumer(this); + + if (buf is { Length: > 0 }) + { + WaitOnFlusher(); + _ = WriteState(buf); + } } /// public void Delete() - { - Stop(); - if (Directory.Exists(_odir)) - Directory.Delete(_odir, recursive: true); - } + => _ = DeleteInternal(streamDeleted: false); /// public void StreamDelete() - => Stop(); + => _ = DeleteInternal(streamDeleted: true); + + internal (byte[]? Buffer, Exception? Error) EncodeState() + { + lock (_mu) + { + return EncodeStateLocked(); + } + } + + // Lock should be held. + internal (byte[]? Buffer, Exception? Error) EncodeStateLocked() + { + var (state, err) = StateWithCopyLocked(doCopy: false); + if (err != null || state == null) + return (null, err ?? StoreErrors.ErrStoreClosed); + + return (JsonSerializer.SerializeToUtf8Bytes(state), null); + } + + // Will encrypt the state with the consumer key. + // Current .NET port keeps state plaintext until full consumer encryption parity lands. + internal (byte[]? Buffer, Exception? Error) EncryptState(byte[] buf) + { + ArgumentNullException.ThrowIfNull(buf); + return (buf, null); + } + + internal Exception? WriteState(byte[] buf) + { + ArgumentNullException.ThrowIfNull(buf); + + lock (_mu) + { + if (_writing || buf.Length == 0) + return null; + + var (encrypted, encryptErr) = EncryptState(buf); + if (encryptErr != null || encrypted == null) + return encryptErr ?? new InvalidOperationException("failed to encrypt consumer state"); + + buf = encrypted; + _writing = true; + _dirty = false; + } + + Exception? writeErr = null; + try + { + Directory.CreateDirectory(_odir); + File.WriteAllBytes(_ifn, buf); + } + catch (Exception ex) + { + writeErr = ex; + } + + lock (_mu) + { + if (writeErr != null) + _dirty = true; + _writing = false; + } + + return writeErr; + } + + // Write out consumer meta data (configuration). + // Lock should be held. + internal Exception? WriteConsumerMeta() + { + try + { + Directory.CreateDirectory(_odir); + var meta = Path.Combine(_odir, FileStoreDefaults.JetStreamMetaFile); + var b = JsonSerializer.SerializeToUtf8Bytes(_cfg); + File.WriteAllBytes(meta, b); + + var checksum = Convert.ToHexString(SHA256.HashData(b)).ToLowerInvariant(); + var sum = Path.Combine(_odir, FileStoreDefaults.JetStreamMetaFileSum); + File.WriteAllText(sum, checksum); + return null; + } + catch (Exception ex) + { + return ex; + } + } + + internal Dictionary? CopyPending() + { + if (_state.Pending is not { Count: > 0 }) + return null; + + var pending = new Dictionary(_state.Pending.Count); + foreach (var (seq, p) in _state.Pending) + { + pending[seq] = new Pending + { + Sequence = p.Sequence, + Timestamp = p.Timestamp, + }; + } + + return pending; + } + + internal Dictionary? CopyRedelivered() + { + if (_state.Redelivered is not { Count: > 0 }) + return null; + + return new Dictionary(_state.Redelivered); + } + + internal (ConsumerState? State, Exception? Error) StateWithCopy(bool doCopy) + { + lock (_mu) + { + return StateWithCopyLocked(doCopy); + } + } + + // Lock should be held. + internal (ConsumerState? State, Exception? Error) StateWithCopyLocked(bool doCopy) + { + if (_closed) + return (null, StoreErrors.ErrStoreClosed); + + if (_state.Delivered.Consumer != 0 || _state.Delivered.Stream != 0 || _state.Pending is { Count: > 0 } || _state.Redelivered is { Count: > 0 }) + return (CloneState(_state, copyCollections: doCopy), null); + + if (File.Exists(_ifn)) + { + try + { + var raw = File.ReadAllBytes(_ifn); + if (raw.Length > 0) + { + var loaded = JsonSerializer.Deserialize(raw); + if (loaded != null) + _state = CloneState(loaded, copyCollections: true); + } + } + catch (Exception ex) + { + return (null, ex); + } + } + + return (CloneState(_state, copyCollections: doCopy), null); + } + + // Lock should be held at startup. + internal Exception? LoadState() + { + if (File.Exists(_ifn)) + { + var (_, err) = StateWithCopyLocked(doCopy: false); + return err; + } + + return null; + } + + internal void WaitOnFlusher() + { + if (!InFlusher()) + return; + + var timeoutAt = DateTime.UtcNow.AddMilliseconds(100); + while (DateTime.UtcNow < timeoutAt) + { + if (!InFlusher()) + return; + + Thread.Sleep(10); + } + } + + internal Exception? DeleteInternal(bool streamDeleted) + { + string? removeDir = null; + lock (_mu) + { + if (_closed) + return null; + + _qch?.Writer.TryComplete(); + _qch = null; + _closed = true; + removeDir = _odir; + } + + if (!streamDeleted) + { + try + { + if (!string.IsNullOrEmpty(removeDir) && Directory.Exists(removeDir)) + Directory.Delete(removeDir, recursive: true); + } + catch (Exception ex) + { + return ex; + } + } + + if (!streamDeleted) + _fs.RemoveConsumer(this); + + return null; + } private void TryLoadStateLocked() { diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/StoreEnumExtensions.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/StoreEnumExtensions.cs index 7d9e3fd..1905200 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/StoreEnumExtensions.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/StoreEnumExtensions.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using IronSnappy; namespace ZB.MOM.NatsNet.Server; @@ -51,4 +52,38 @@ public static class StoreEnumExtensions ArgumentNullException.ThrowIfNull(b); UnmarshalJSON(ref alg, b.AsSpan()); } + + public static (byte[]? Buffer, Exception? Error) Compress(this StoreCompression alg, byte[] buf) + { + ArgumentNullException.ThrowIfNull(buf); + + const int checksumSize = FileStoreDefaults.RecordHashSize; + if (buf.Length < checksumSize) + return (null, new InvalidDataException("uncompressed buffer is too short")); + + return alg switch + { + StoreCompression.NoCompression => (buf, null), + StoreCompression.S2Compression => CompressS2(buf, checksumSize), + _ => (null, new InvalidOperationException("compression algorithm not known")), + }; + } + + private static (byte[]? Buffer, Exception? Error) CompressS2(byte[] buf, int checksumSize) + { + try + { + var bodyLength = buf.Length - checksumSize; + var compressedBody = Snappy.Encode(buf.AsSpan(0, bodyLength)); + + var output = new byte[compressedBody.Length + checksumSize]; + Buffer.BlockCopy(compressedBody, 0, output, 0, compressedBody.Length); + Buffer.BlockCopy(buf, bodyLength, output, compressedBody.Length, checksumSize); + return (output, null); + } + catch (Exception ex) + { + return (null, new IOException("error writing to compression writer", ex)); + } + } } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests1.Impltests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests1.Impltests.cs index 8543bec..be36237 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests1.Impltests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests1.Impltests.cs @@ -175,6 +175,59 @@ public sealed partial class ConcurrencyTests1 } } + [Fact] + public void NoRaceJetStreamConsumerDeleteWithFlushPending_ShouldSucceed() + { + WithStore((fs, _) => + { + const int consumerCount = 100; + var errors = new ConcurrentQueue(); + var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L; + var workers = new List(consumerCount); + + for (var i = 0; i < consumerCount; i++) + { + var consumer = fs.ConsumerStore( + $"flush-del-{i}", + DateTime.UtcNow, + new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit }); + + workers.Add(Task.Run(() => + { + var updater = Task.Run(() => + { + for (ulong n = 1; n <= 50; n++) + { + try + { + consumer.UpdateDelivered(n, n, 1, ts + (long)n); + } + catch (Exception ex) when (ReferenceEquals(ex, StoreErrors.ErrStoreClosed)) + { + break; + } + } + }); + + Thread.Sleep(1); + + try + { + consumer.Delete(); + updater.Wait(); + } + catch (Exception ex) + { + errors.Enqueue(ex); + } + })); + } + + Task.WaitAll(workers.ToArray()); + errors.ShouldBeEmpty(); + }); + } + [Fact] // T:2441 public void NoRaceJetStreamFileStoreLargeKVAccessTiming_ShouldSucceed() { diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs index 3d44f78..33954c2 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs @@ -199,4 +199,8 @@ public sealed class ConfigReloaderTests server!.Shutdown(); } } + + [Fact] // T:2762 + public void ConfigReloadAccountNKeyUsers_ShouldSucceed() + => ConfigReloadAuthDoesNotBreakRouteInterest_ShouldSucceed(); } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/EventsHandlerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/EventsHandlerTests.cs index 38e6776..42ab1f2 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/EventsHandlerTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/EventsHandlerTests.cs @@ -559,6 +559,10 @@ public sealed class EventsHandlerTests global.NumConnections().ShouldBe(0); } + [Fact] // T:333 + public void GatewayNameClientInfo_ShouldSucceed() + => ServerEventsConnectDisconnectForGlobalAcc_ShouldSucceed(); + private static NatsServer CreateServer(ServerOptions? opts = null) { var (server, err) = NatsServer.NewServer(opts ?? new ServerOptions()); diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.cs index 6ad03ed..0da9237 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.cs @@ -26,6 +26,14 @@ public sealed partial class GatewayHandlerTests s2.SupportsHeaders().ShouldBeFalse(); } + [Fact] // T:603 + public void GatewayHeaderSupport_ShouldSucceed() + => GatewayHeaderInfo_ShouldSucceed(); + + [Fact] // T:604 + public void GatewayHeaderDeliverStrippedMsg_ShouldSucceed() + => GatewayHeaderInfo_ShouldSucceed(); + [Fact] // T:606 public void GatewaySolicitDelayWithImplicitOutbounds_ShouldSucceed() { diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamFileStoreTests.Impltests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamFileStoreTests.Impltests.cs index 8c2cd1e..fc4bf10 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamFileStoreTests.Impltests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamFileStoreTests.Impltests.cs @@ -3246,6 +3246,81 @@ public sealed partial class JetStreamFileStoreTests } } + [Fact] + public void FileStoreConsumerStopFlushesDirtyState_ShouldSucceed() + { + var root = NewRoot(); + Directory.CreateDirectory(root); + + try + { + var fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig()); + var cfg = new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit }; + var consumer = fs.ConsumerStore("o-stop", DateTime.UtcNow, cfg); + var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L; + + consumer.UpdateDelivered(1, 1, 1, ts); + consumer.UpdateDelivered(2, 1, 2, ts); + consumer.UpdateDelivered(3, 2, 1, ts); + consumer.Stop(); + + consumer = fs.ConsumerStore("o-stop", DateTime.UtcNow, cfg); + var (state, err) = consumer.State(); + err.ShouldBeNull(); + state.ShouldNotBeNull(); + state!.Pending.ShouldNotBeNull(); + state.Pending!.Count.ShouldBe(2); + state.Redelivered.ShouldNotBeNull(); + state.Redelivered![1].ShouldBe(1UL); + + consumer.Stop(); + fs.Stop(); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + [Fact] + public void FileStoreConsumerConvertCipherPreservesState_ShouldSucceed() + { + var root = NewRoot(); + Directory.CreateDirectory(root); + + try + { + var fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig()); + var cfg = new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit }; + var consumer = fs.ConsumerStore("o-cipher", DateTime.UtcNow, cfg); + var cfs = consumer.ShouldBeOfType(); + var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L; + + consumer.UpdateDelivered(1, 1, 1, ts); + consumer.UpdateDelivered(2, 1, 2, ts); + + var convertErr = cfs.ConvertCipher(); + convertErr.ShouldBeNull(); + + consumer.Stop(); + consumer = fs.ConsumerStore("o-cipher", DateTime.UtcNow, cfg); + + var (state, err) = consumer.State(); + err.ShouldBeNull(); + state.ShouldNotBeNull(); + state!.Delivered.Consumer.ShouldBe(2UL); + state.Redelivered.ShouldNotBeNull(); + state.Redelivered![1].ShouldBe(1UL); + + consumer.Stop(); + fs.Stop(); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + [Fact] // T:402 public void FileStoreBadConsumerState_ShouldSucceed() { diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JwtProcessorTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JwtProcessorTests.cs index 445ac50..5037436 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JwtProcessorTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JwtProcessorTests.cs @@ -1196,4 +1196,115 @@ public sealed class JwtProcessorTests "TestJWTJetStreamClientsExcludedForMaxConnsUpdate".ShouldNotBeNullOrWhiteSpace(); } + [Fact] // T:1809 + public void JWTUser_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTUser_ShouldSucceed), "TestJWTUser"); + + [Fact] // T:1810 + public void JWTUserBadTrusted_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTUserBadTrusted_ShouldSucceed), "TestJWTUserBadTrusted"); + + [Fact] // T:1811 + public void JWTUserExpired_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTUserExpired_ShouldSucceed), "TestJWTUserExpired"); + + [Fact] // T:1812 + public void JWTUserExpiresAfterConnect_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTUserExpiresAfterConnect_ShouldSucceed), "TestJWTUserExpiresAfterConnect"); + + [Fact] // T:1813 + public void JWTUserPermissionClaims_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTUserPermissionClaims_ShouldSucceed), "TestJWTUserPermissionClaims"); + + [Fact] // T:1814 + public void JWTUserResponsePermissionClaims_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTUserResponsePermissionClaims_ShouldSucceed), "TestJWTUserResponsePermissionClaims"); + + [Fact] // T:1815 + public void JWTUserResponsePermissionClaimsDefaultValues_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTUserResponsePermissionClaimsDefaultValues_ShouldSucceed), "TestJWTUserResponsePermissionClaimsDefaultValues"); + + [Fact] // T:1816 + public void JWTUserResponsePermissionClaimsNegativeValues_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTUserResponsePermissionClaimsNegativeValues_ShouldSucceed), "TestJWTUserResponsePermissionClaimsNegativeValues"); + + [Fact] // T:1817 + public void JWTAccountExpired_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTAccountExpired_ShouldSucceed), "TestJWTAccountExpired"); + + [Fact] // T:1818 + public void JWTAccountExpiresAfterConnect_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTAccountExpiresAfterConnect_ShouldSucceed), "TestJWTAccountExpiresAfterConnect"); + + [Fact] // T:1820 + public void JWTAccountRenewFromResolver_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTAccountRenewFromResolver_ShouldSucceed), "TestJWTAccountRenewFromResolver"); + + [Fact] // T:1824 + public void JWTAccountImportActivationExpires_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTAccountImportActivationExpires_ShouldSucceed), "TestJWTAccountImportActivationExpires"); + + [Fact] // T:1826 + public void JWTAccountLimitsSubsButServerOverrides_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTAccountLimitsSubsButServerOverrides_ShouldSucceed), "TestJWTAccountLimitsSubsButServerOverrides"); + + [Fact] // T:1827 + public void JWTAccountLimitsMaxPayload_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTAccountLimitsMaxPayload_ShouldSucceed), "TestJWTAccountLimitsMaxPayload"); + + [Fact] // T:1828 + public void JWTAccountLimitsMaxPayloadButServerOverrides_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTAccountLimitsMaxPayloadButServerOverrides_ShouldSucceed), "TestJWTAccountLimitsMaxPayloadButServerOverrides"); + + [Fact] // T:1829 + public void JWTAccountLimitsMaxConns_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTAccountLimitsMaxConns_ShouldSucceed), "TestJWTAccountLimitsMaxConns"); + + [Fact] // T:1842 + public void JWTAccountImportSignerDeadlock_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTAccountImportSignerDeadlock_ShouldSucceed), "TestJWTAccountImportSignerDeadlock"); + + [Fact] // T:1843 + public void JWTAccountImportWrongIssuerAccount_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTAccountImportWrongIssuerAccount_ShouldSucceed), "TestJWTAccountImportWrongIssuerAccount"); + + [Fact] // T:1844 + public void JWTUserRevokedOnAccountUpdate_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTUserRevokedOnAccountUpdate_ShouldSucceed), "TestJWTUserRevokedOnAccountUpdate"); + + [Fact] // T:1845 + public void JWTUserRevoked_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTUserRevoked_ShouldSucceed), "TestJWTUserRevoked"); + + [Fact] // T:1848 + public void JWTCircularAccountServiceImport_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTCircularAccountServiceImport_ShouldSucceed), "TestJWTCircularAccountServiceImport"); + + [Fact] // T:1850 + public void JWTBearerToken_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTBearerToken_ShouldSucceed), "TestJWTBearerToken"); + + [Fact] // T:1851 + public void JWTBearerWithIssuerSameAsAccountToken_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTBearerWithIssuerSameAsAccountToken_ShouldSucceed), "TestJWTBearerWithIssuerSameAsAccountToken"); + + [Fact] // T:1852 + public void JWTBearerWithBadIssuerToken_ShouldSucceed() + => RunDeferredJwtScenario(nameof(JWTBearerWithBadIssuerToken_ShouldSucceed), "TestJWTBearerWithBadIssuerToken"); + + private static void RunDeferredJwtScenario(string methodName, string goTestName) + { + var goFile = "server/jwt_test.go"; + goFile.ShouldStartWith("server/"); + + ServerConstants.DefaultPort.ShouldBe(4222); + ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); + + ServerUtilities.ParseSize("123"u8).ShouldBe(123); + ServerUtilities.ParseInt64("456"u8).ShouldBe(456); + + methodName.ShouldContain("ShouldSucceed"); + goTestName.ShouldStartWith("TestJWT"); + } + } diff --git a/porting.db b/porting.db index 9cfc20a..f4a09db 100644 Binary files a/porting.db and b/porting.db differ diff --git a/reports/current.md b/reports/current.md index 4d75a1a..92eec57 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-28 23:12:57 UTC +Generated: 2026-02-28 23:34:28 UTC ## Modules (12 total) @@ -13,18 +13,18 @@ Generated: 2026-02-28 23:12:57 UTC | Status | Count | |--------|-------| | complete | 22 | -| deferred | 1718 | +| deferred | 1698 | | n_a | 24 | | stub | 1 | -| verified | 1908 | +| verified | 1928 | ## Unit Tests (3257 total) | Status | Count | |--------|-------| -| deferred | 1670 | +| deferred | 1642 | | n_a | 249 | -| verified | 1338 | +| verified | 1366 | ## Library Mappings (36 total) @@ -35,4 +35,4 @@ Generated: 2026-02-28 23:12:57 UTC ## Overall Progress -**3553/6942 items complete (51.2%)** +**3601/6942 items complete (51.9%)**