Eliminate PortTracker stub backlog by implementing Raft/file-store/stream/server/client/OCSP stubs and adding coverage. This makes all tracked stub features/tests executable and verified in the current porting phase.

This commit is contained in:
Joseph Doherty
2026-02-27 08:56:26 -05:00
parent ba4f41cf71
commit 8849265780
33 changed files with 2938 additions and 407 deletions

View File

@@ -0,0 +1,39 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using Shouldly;
using ZB.MOM.NatsNet.Server;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed class CompressionInfoTests
{
[Fact]
public void MarshalMetadata_UnmarshalMetadata_ShouldRoundTrip()
{
var ci = new CompressionInfo
{
Type = StoreCompression.S2Compression,
Original = 12345,
Compressed = 6789,
};
var payload = ci.MarshalMetadata();
payload.Length.ShouldBeGreaterThan(4);
var copy = new CompressionInfo();
var consumed = copy.UnmarshalMetadata(payload);
consumed.ShouldBe(payload.Length);
copy.Type.ShouldBe(StoreCompression.S2Compression);
copy.Original.ShouldBe(12345UL);
copy.Compressed.ShouldBe(6789UL);
}
[Fact]
public void UnmarshalMetadata_InvalidPrefix_ShouldReturnZero()
{
var ci = new CompressionInfo();
ci.UnmarshalMetadata([1, 2, 3, 4]).ShouldBe(0);
}
}

View File

@@ -0,0 +1,74 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using Shouldly;
using ZB.MOM.NatsNet.Server;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed class ConsumerFileStoreTests
{
[Fact]
public void UpdateDelivered_UpdateAcks_AndReload_ShouldPersistState()
{
var root = Path.Combine(Path.GetTempPath(), $"cfs-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
try
{
var fs = NewStore(root);
var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
var cs = (ConsumerFileStore)fs.ConsumerStore("D", DateTime.UtcNow, cfg);
cs.SetStarting(0);
cs.UpdateDelivered(1, 1, 1, 123);
cs.UpdateDelivered(2, 2, 1, 456);
cs.UpdateAcks(1, 1);
var (state, err) = cs.State();
err.ShouldBeNull();
state.ShouldNotBeNull();
state!.Delivered.Consumer.ShouldBe(2UL);
state.AckFloor.Consumer.ShouldBe(1UL);
cs.Stop();
var odir = Path.Combine(root, FileStoreDefaults.ConsumerDir, "D");
var loaded = new ConsumerFileStore(
fs,
new FileConsumerInfo { Name = "D", Created = DateTime.UtcNow, Config = cfg },
"D",
odir);
var (loadedState, loadedErr) = loaded.State();
loadedErr.ShouldBeNull();
loadedState.ShouldNotBeNull();
loadedState!.Delivered.Consumer.ShouldBe(2UL);
loadedState.AckFloor.Consumer.ShouldBe(1UL);
loaded.Delete();
Directory.Exists(odir).ShouldBeFalse();
fs.Stop();
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
private static JetStreamFileStore NewStore(string root)
{
return new JetStreamFileStore(
new FileStoreConfig { StoreDir = root },
new FileStreamInfo
{
Created = DateTime.UtcNow,
Config = new StreamConfig
{
Name = "S",
Storage = StorageType.FileStorage,
Subjects = ["foo"],
},
});
}
}

View File

@@ -1,50 +1,100 @@
// Copyright 2020-2025 The NATS Authors
// Copyright 2020-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Mirrors server/jetstream_errors_test.go in the NATS server Go source.
//
// All 4 tests are deferred:
// T:1381 — TestIsNatsErr: uses IsNatsErr(error, ...) where the Go version accepts
// arbitrary error interface values (including plain errors.New("x") which
// evaluates to false). The .NET JsApiErrors.IsNatsError only accepts JsApiError?
// and the "NewJS*" factory constructors (NewJSRestoreSubscribeFailedError etc.)
// that populate Description templates from tags have not been ported yet.
// T:1382 — TestApiError_Error: uses ApiErrors[JSClusterNotActiveErr].Error() — the Go
// ApiErrors map and per-error .Error() method (returns "description (errCode)")
// differs from the .NET JsApiErrors.ClusterNotActive.ToString() convention.
// T:1383 — TestApiError_NewWithTags: uses NewJSRestoreSubscribeFailedError with tag
// substitution — factory constructors not yet ported.
// T:1384 — TestApiError_NewWithUnless: uses NewJSStreamRestoreError, Unless() helper,
// NewJSPeerRemapError — not yet ported.
using Shouldly;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
/// <summary>
/// Tests for JetStream API error types and IsNatsErr helper.
/// Tests for JetStream API error helpers.
/// Mirrors server/jetstream_errors_test.go.
/// All tests deferred pending port of Go factory constructors and tag-substitution system.
/// </summary>
public sealed class JetStreamErrorsTests
{
[Fact(Skip = "deferred: NewJS* factory constructors and IsNatsErr(error) not yet ported")] // T:1381
public void IsNatsErr_ShouldSucceed() { }
[Fact] // T:1381
public void IsNatsErr_ShouldSucceed()
{
JsApiErrors.IsNatsErr(
JsApiErrors.NotEnabledForAccount,
JsApiErrors.NotEnabledForAccount.ErrCode).ShouldBeTrue();
[Fact(Skip = "deferred: ApiErrors map and .Error() method not yet ported")] // T:1382
public void ApiError_Error_ShouldSucceed() { }
JsApiErrors.IsNatsErr(
JsApiErrors.NotEnabledForAccount,
JsApiErrors.ClusterNotActive.ErrCode).ShouldBeFalse();
[Fact(Skip = "deferred: NewJSRestoreSubscribeFailedError with tag substitution not yet ported")] // T:1383
public void ApiError_NewWithTags_ShouldSucceed() { }
JsApiErrors.IsNatsErr(
JsApiErrors.NotEnabledForAccount,
JsApiErrors.ClusterNotActive.ErrCode,
JsApiErrors.ClusterNotAvail.ErrCode).ShouldBeFalse();
[Fact(Skip = "deferred: NewJSStreamRestoreError / Unless() helper not yet ported")] // T:1384
public void ApiError_NewWithUnless_ShouldSucceed() { }
JsApiErrors.IsNatsErr(
JsApiErrors.NotEnabledForAccount,
JsApiErrors.ClusterNotActive.ErrCode,
JsApiErrors.NotEnabledForAccount.ErrCode).ShouldBeTrue();
JsApiErrors.IsNatsErr(
new JsApiError { ErrCode = JsApiErrors.NotEnabledForAccount.ErrCode },
1,
JsApiErrors.ClusterNotActive.ErrCode,
JsApiErrors.NotEnabledForAccount.ErrCode).ShouldBeTrue();
JsApiErrors.IsNatsErr(
new JsApiError { ErrCode = JsApiErrors.NotEnabledForAccount.ErrCode },
1,
2,
JsApiErrors.ClusterNotActive.ErrCode).ShouldBeFalse();
JsApiErrors.IsNatsErr(null, JsApiErrors.ClusterNotActive.ErrCode).ShouldBeFalse();
JsApiErrors.IsNatsErr(new InvalidOperationException("x"), JsApiErrors.ClusterNotActive.ErrCode).ShouldBeFalse();
}
[Fact] // T:1382
public void ApiError_Error_ShouldSucceed()
{
JsApiErrors.Error(JsApiErrors.ClusterNotActive).ShouldBe("JetStream not in clustered mode (10006)");
}
[Fact] // T:1383
public void ApiError_NewWithTags_ShouldSucceed()
{
var ne = JsApiErrors.NewJSRestoreSubscribeFailedError(new Exception("failed error"), "the.subject");
ne.Description.ShouldBe("JetStream unable to subscribe to restore snapshot the.subject: failed error");
ReferenceEquals(ne, JsApiErrors.RestoreSubscribeFailed).ShouldBeFalse();
}
[Fact] // T:1384
public void ApiError_NewWithUnless_ShouldSucceed()
{
var notEnabled = JsApiErrors.NotEnabledForAccount.ErrCode;
var streamRestore = JsApiErrors.StreamRestore.ErrCode;
var peerRemap = JsApiErrors.PeerRemap.ErrCode;
JsApiErrors.IsNatsErr(
JsApiErrors.NewJSStreamRestoreError(
new Exception("failed error"),
JsApiErrors.Unless(JsApiErrors.NotEnabledForAccount)),
notEnabled).ShouldBeTrue();
JsApiErrors.IsNatsErr(
JsApiErrors.NewJSStreamRestoreError(new Exception("failed error")),
streamRestore).ShouldBeTrue();
JsApiErrors.IsNatsErr(
JsApiErrors.NewJSStreamRestoreError(
new Exception("failed error"),
JsApiErrors.Unless(new Exception("other error"))),
streamRestore).ShouldBeTrue();
JsApiErrors.IsNatsErr(
JsApiErrors.NewJSPeerRemapError(JsApiErrors.Unless(JsApiErrors.NotEnabledForAccount)),
notEnabled).ShouldBeTrue();
JsApiErrors.IsNatsErr(
JsApiErrors.NewJSPeerRemapError(JsApiErrors.Unless(null)),
peerRemap).ShouldBeTrue();
JsApiErrors.IsNatsErr(
JsApiErrors.NewJSPeerRemapError(JsApiErrors.Unless(new Exception("other error"))),
peerRemap).ShouldBeTrue();
}
}

View File

@@ -0,0 +1,76 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using Shouldly;
using ZB.MOM.NatsNet.Server;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed class JetStreamFileStoreTests
{
[Fact]
public void StoreMsg_LoadAndPurge_ShouldRoundTrip()
{
var root = Path.Combine(Path.GetTempPath(), $"fs-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
try
{
var fs = NewStore(root);
var (seq1, _) = fs.StoreMsg("foo", [1], [2, 3], 0);
var (seq2, _) = fs.StoreMsg("bar", null, [4, 5], 0);
seq1.ShouldBe(1UL);
seq2.ShouldBe(2UL);
fs.State().Msgs.ShouldBe(2UL);
var msg = fs.LoadMsg(1, null);
msg.ShouldNotBeNull();
msg!.Subject.ShouldBe("foo");
fs.SubjectForSeq(2).Subject.ShouldBe("bar");
fs.SubjectsTotals(string.Empty).Count.ShouldBe(2);
var (removed, remErr) = fs.RemoveMsg(1);
removed.ShouldBeTrue();
remErr.ShouldBeNull();
fs.State().Msgs.ShouldBe(1UL);
var (purged, purgeErr) = fs.Purge();
purgeErr.ShouldBeNull();
purged.ShouldBe(1UL);
fs.State().Msgs.ShouldBe(0UL);
var (snapshot, snapErr) = fs.Snapshot(TimeSpan.FromSeconds(1), includeConsumers: false, checkMsgs: false);
snapErr.ShouldBeNull();
snapshot.ShouldNotBeNull();
snapshot!.Reader.ShouldNotBeNull();
var (total, reported, utilErr) = fs.Utilization();
utilErr.ShouldBeNull();
total.ShouldBe(reported);
fs.Stop();
}
finally
{
Directory.Delete(root, recursive: true);
}
}
private static JetStreamFileStore NewStore(string root)
{
return new JetStreamFileStore(
new FileStoreConfig { StoreDir = root },
new FileStreamInfo
{
Created = DateTime.UtcNow,
Config = new StreamConfig
{
Name = "S",
Storage = StorageType.FileStorage,
Subjects = ["foo", "bar"],
},
});
}
}

View File

@@ -0,0 +1,38 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using Shouldly;
using ZB.MOM.NatsNet.Server;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed class NatsConsumerTests
{
[Fact]
public void Create_SetLeader_UpdateConfig_AndStop_ShouldBehave()
{
var account = new Account { Name = "A" };
var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Storage = StorageType.FileStorage };
var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
stream.ShouldNotBeNull();
var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.CreateOrUpdate, null);
consumer.ShouldNotBeNull();
consumer!.IsLeader().ShouldBeFalse();
consumer.SetLeader(true, 3);
consumer.IsLeader().ShouldBeTrue();
var updated = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckAll };
consumer.UpdateConfig(updated);
consumer.GetConfig().AckPolicy.ShouldBe(AckPolicy.AckAll);
var info = consumer.GetInfo();
info.Stream.ShouldBe("S");
info.Name.ShouldBe("D");
consumer.Stop();
consumer.IsLeader().ShouldBeFalse();
}
}

View File

@@ -0,0 +1,43 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using Shouldly;
using ZB.MOM.NatsNet.Server;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed class NatsStreamTests
{
[Fact]
public void Create_SetLeader_Purge_AndSeal_ShouldBehave()
{
var account = new Account { Name = "A" };
var streamCfg = new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], Storage = StorageType.FileStorage };
var memCfg = streamCfg.Clone();
memCfg.Storage = StorageType.MemoryStorage;
var store = new JetStreamMemStore(memCfg);
store.StoreMsg("orders.new", null, [1, 2], 0);
var stream = NatsStream.Create(account, streamCfg, null, store, null, null);
stream.ShouldNotBeNull();
stream!.IsLeader().ShouldBeFalse();
stream.SetLeader(true, 7);
stream.IsLeader().ShouldBeTrue();
stream.State().Msgs.ShouldBe(1UL);
stream.Purge();
stream.State().Msgs.ShouldBe(0UL);
stream.IsSealed().ShouldBeFalse();
stream.Seal();
stream.IsSealed().ShouldBeTrue();
stream.GetAccount().Name.ShouldBe("A");
stream.GetInfo().Config.Name.ShouldBe("ORDERS");
stream.Delete();
stream.IsLeader().ShouldBeFalse();
}
}

View File

@@ -0,0 +1,140 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Threading.Channels;
using Shouldly;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed class RaftTypesTests
{
[Fact]
public void Raft_Methods_ShouldProvideNonStubBehavior()
{
var raft = new Raft
{
Id = "N1",
GroupName = "RG",
AccName = "ACC",
StateValue = (int)RaftState.Leader,
LeaderId = "N1",
Csz = 3,
Qn = 2,
PIndex = 10,
Commit = 8,
Applied_ = 6,
Processed_ = 7,
PApplied = 9,
WalBytes = 128,
Peers_ = new Dictionary<string, Lps>
{
["N2"] = new() { Ts = DateTime.UtcNow, Kp = true, Li = 9 },
},
ApplyQ_ = new IpQueue<CommittedEntry>("apply-q"),
LeadC = Channel.CreateUnbounded<bool>(),
Quit = Channel.CreateUnbounded<bool>(),
};
raft.Propose([1, 2, 3]);
raft.ForwardProposal([4, 5]);
raft.ProposeMulti([new Entry { Data = [6] }]);
raft.PropQ.ShouldNotBeNull();
raft.PropQ!.Len().ShouldBe(3);
raft.InstallSnapshot([9, 9], force: false);
raft.SendSnapshot([8, 8, 8]);
raft.CreateSnapshotCheckpoint(force: false).ShouldBeOfType<Checkpoint>();
raft.NeedSnapshot().ShouldBeTrue();
raft.Applied(5).ShouldBe((1UL, 128UL));
raft.Processed(11, 10).ShouldBe((11UL, 128UL));
raft.Size().ShouldBe((11UL, 128UL));
raft.Progress().ShouldBe((10UL, 8UL, 10UL));
raft.Leader().ShouldBeTrue();
raft.LeaderSince().ShouldNotBeNull();
raft.Quorum().ShouldBeTrue();
raft.Current().ShouldBeTrue();
raft.Healthy().ShouldBeTrue();
raft.Term().ShouldBe(raft.Term_);
raft.Leaderless().ShouldBeFalse();
raft.GroupLeader().ShouldBe("N1");
raft.SetObserver(true);
raft.IsObserver().ShouldBeTrue();
raft.Campaign();
raft.State().ShouldBe(RaftState.Candidate);
raft.CampaignImmediately();
raft.StepDown("N2");
raft.State().ShouldBe(RaftState.Follower);
raft.ProposeKnownPeers(["P1", "P2"]);
raft.Peers().Count.ShouldBe(3);
raft.ProposeAddPeer("P3");
raft.ClusterSize().ShouldBeGreaterThan(1);
raft.ProposeRemovePeer("P2");
raft.Peers().Count.ShouldBe(3);
raft.MembershipChangeInProgress().ShouldBeTrue();
raft.AdjustClusterSize(5);
raft.ClusterSize().ShouldBe(5);
raft.AdjustBootClusterSize(4);
raft.ClusterSize().ShouldBe(4);
raft.ApplyQ().ShouldNotBeNull();
raft.PauseApply();
raft.Paused.ShouldBeTrue();
raft.ResumeApply();
raft.Paused.ShouldBeFalse();
raft.DrainAndReplaySnapshot().ShouldBeTrue();
raft.LeadChangeC().ShouldNotBeNull();
raft.QuitC().ShouldNotBeNull();
raft.Created().ShouldBe(raft.Created_);
raft.ID().ShouldBe("N1");
raft.Group().ShouldBe("RG");
raft.GetTrafficAccountName().ShouldBe("ACC");
raft.RecreateInternalSubs();
raft.Stop();
raft.WaitForStop();
raft.Delete();
raft.IsDeleted().ShouldBeTrue();
}
[Fact]
public void Checkpoint_Methods_ShouldRoundTripSnapshotData()
{
var node = new Raft
{
Id = "NODE",
PTerm = 3,
AReply = "_R_",
};
var checkpoint = new Checkpoint
{
Node = node,
Term = 5,
Applied = 11,
PApplied = 7,
SnapFile = Path.Combine(Path.GetTempPath(), $"checkpoint-{Guid.NewGuid():N}.bin"),
};
var written = checkpoint.InstallSnapshot([1, 2, 3, 4]);
written.ShouldBe(4UL);
var loaded = checkpoint.LoadLastSnapshot();
loaded.ShouldBe([1, 2, 3, 4]);
var seq = checkpoint.AppendEntriesSeq().ToList();
seq.Count.ShouldBe(1);
seq[0].Error.ShouldBeNull();
seq[0].Entry.Leader.ShouldBe("NODE");
seq[0].Entry.TermV.ShouldBe(5UL);
seq[0].Entry.Commit.ShouldBe(11UL);
seq[0].Entry.PIndex.ShouldBe(7UL);
checkpoint.Abort();
File.Exists(checkpoint.SnapFile).ShouldBeFalse();
}
}

View File

@@ -0,0 +1,31 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using Shouldly;
using ZB.MOM.NatsNet.Server;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed class WaitQueueTests
{
[Fact]
public void Add_Peek_Pop_IsFull_ShouldBehaveAsFifo()
{
var q = new WaitQueue();
q.Peek().ShouldBeNull();
q.Pop().ShouldBeNull();
q.Add(new WaitingRequest { Subject = "A", N = 1 });
q.Add(new WaitingRequest { Subject = "B", N = 2 });
q.Len.ShouldBe(2);
q.IsFull(2).ShouldBeTrue();
q.Peek()!.Subject.ShouldBe("A");
q.Pop()!.Subject.ShouldBe("A");
q.Pop()!.Subject.ShouldBe("B");
q.Len.ShouldBe(0);
q.IsFull(1).ShouldBeFalse();
}
}