// Copyright 2024-2025 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_versioning_test.go in the NATS server Go source. using Shouldly; namespace ZB.MOM.NatsNet.Server.Tests.JetStream; /// /// Unit tests for JetStream API level versioning helpers. /// Mirrors server/jetstream_versioning_test.go. /// Tests 1803–1808 (TestJetStreamMetadataMutations, TestJetStreamMetadataStreamRestoreAndRestart, /// TestJetStreamMetadataStreamRestoreAndRestartCluster, TestJetStreamApiErrorOnRequiredApiLevel, /// TestJetStreamApiErrorOnRequiredApiLevelDirectGet, TestJetStreamApiErrorOnRequiredApiLevelPullConsumerNextMsg) /// all require a running JetStream server and are deferred. /// public sealed class JetStreamVersioningTests { // ------------------------------------------------------------------------- // Helpers (mirrors module-level helpers in Go test file) // ------------------------------------------------------------------------- private static Dictionary MetadataAtLevel(string featureLevel) => new() { [JetStreamVersioning.JsRequiredLevelMetadataKey] = featureLevel }; private static Dictionary MetadataPrevious() => new() { [JetStreamVersioning.JsRequiredLevelMetadataKey] = "previous-level" }; // ------------------------------------------------------------------------- // T:1791 — TestGetAndSupportsRequiredApiLevel // ------------------------------------------------------------------------- [Fact] // T:1791 public void GetAndSupportsRequiredApiLevel_VariousInputs_ReturnsExpected() { // getRequiredApiLevel JetStreamVersioning.GetRequiredApiLevel(null).ShouldBe(string.Empty); JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); JetStreamVersioning.GetRequiredApiLevel(MetadataAtLevel("1")).ShouldBe("1"); JetStreamVersioning.GetRequiredApiLevel(MetadataAtLevel("text")).ShouldBe("text"); // supportsRequiredApiLevel JetStreamVersioning.SupportsRequiredApiLevel(null).ShouldBeTrue(); JetStreamVersioning.SupportsRequiredApiLevel(new Dictionary()).ShouldBeTrue(); JetStreamVersioning.SupportsRequiredApiLevel(MetadataAtLevel("1")).ShouldBeTrue(); JetStreamVersioning.SupportsRequiredApiLevel( MetadataAtLevel(JetStreamVersioning.JsApiLevel.ToString())).ShouldBeTrue(); JetStreamVersioning.SupportsRequiredApiLevel(MetadataAtLevel("text")).ShouldBeFalse(); } // ------------------------------------------------------------------------- // T:1792 — TestJetStreamSetStaticStreamMetadata // ------------------------------------------------------------------------- [Fact] // T:1792 public void SetStaticStreamMetadata_VariousConfigs_SetsCorrectApiLevel() { var cases = new[] { ("empty", new StreamConfig(), "0"), ("overwrite-user-provided", new StreamConfig { Metadata = MetadataPrevious() }, "0"), ("AllowMsgTTL", new StreamConfig { AllowMsgTTL = true }, "1"), ("SubjectDeleteMarkerTTL", new StreamConfig { SubjectDeleteMarkerTTL = TimeSpan.FromSeconds(1) }, "1"), ("AllowMsgCounter", new StreamConfig { AllowMsgCounter = true }, "2"), ("AllowAtomicPublish", new StreamConfig { AllowAtomicPublish = true }, "2"), ("AllowMsgSchedules", new StreamConfig { AllowMsgSchedules = true }, "2"), ("AsyncPersistMode", new StreamConfig { PersistMode = PersistModeType.AsyncPersistMode }, "2"), }; foreach (var (desc, cfg, expectedLevel) in cases) { JetStreamVersioning.SetStaticStreamMetadata(cfg); var level = cfg.Metadata![JetStreamVersioning.JsRequiredLevelMetadataKey]; level.ShouldBe(expectedLevel, $"case: {desc}"); // Ensure we do not exceed the server API level. int.Parse(level).ShouldBeLessThanOrEqualTo(JetStreamVersioning.JsApiLevel, customMessage: $"case: {desc}"); } } // ------------------------------------------------------------------------- // T:1793 — TestJetStreamSetStaticStreamMetadataRemoveDynamicFields // ------------------------------------------------------------------------- [Fact] // T:1793 public void SetStaticStreamMetadata_RemovesDynamicFields() { var cfg = new StreamConfig { Metadata = new Dictionary { [JetStreamVersioning.JsServerVersionMetadataKey] = "dynamic-version", [JetStreamVersioning.JsServerLevelMetadataKey] = "dynamic-level", } }; JetStreamVersioning.SetStaticStreamMetadata(cfg); cfg.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerVersionMetadataKey); cfg.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerLevelMetadataKey); cfg.Metadata[JetStreamVersioning.JsRequiredLevelMetadataKey].ShouldBe("0"); } // ------------------------------------------------------------------------- // T:1794 — TestJetStreamSetDynamicStreamMetadata // ------------------------------------------------------------------------- [Fact] // T:1794 public void SetDynamicStreamMetadata_DoesNotMutateOriginal_AddsVersionFields() { var cfg = new StreamConfig { Metadata = MetadataAtLevel("0") }; var newCfg = JetStreamVersioning.SetDynamicStreamMetadata(cfg); // Original must NOT have dynamic fields. cfg.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerVersionMetadataKey); cfg.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerLevelMetadataKey); // New copy must have dynamic fields. newCfg.Metadata![JetStreamVersioning.JsRequiredLevelMetadataKey].ShouldBe("0"); newCfg.Metadata[JetStreamVersioning.JsServerVersionMetadataKey].ShouldBe(ServerConstants.Version); newCfg.Metadata[JetStreamVersioning.JsServerLevelMetadataKey] .ShouldBe(JetStreamVersioning.JsApiLevel.ToString()); } // ------------------------------------------------------------------------- // T:1795 — TestJetStreamCopyStreamMetadata // ------------------------------------------------------------------------- [Fact] // T:1795 public void CopyStreamMetadata_VariousScenarios_CopiesRequiredLevelKey() { // no-previous-ignore: when prevCfg is null, key must be absent var cfg1 = new StreamConfig { Metadata = MetadataAtLevel("-1") }; JetStreamVersioning.CopyStreamMetadata(cfg1, null); (cfg1.Metadata?.ContainsKey(JetStreamVersioning.JsRequiredLevelMetadataKey) ?? false).ShouldBeFalse(); // nil-previous-metadata-ignore: prevCfg has null Metadata var cfg2 = new StreamConfig { Metadata = MetadataAtLevel("-1") }; JetStreamVersioning.CopyStreamMetadata(cfg2, new StreamConfig { Metadata = null }); (cfg2.Metadata?.ContainsKey(JetStreamVersioning.JsRequiredLevelMetadataKey) ?? false).ShouldBeFalse(); // nil-current-metadata-ignore: cfg has null Metadata — should not throw var cfg3 = new StreamConfig { Metadata = null }; JetStreamVersioning.CopyStreamMetadata(cfg3, new StreamConfig { Metadata = MetadataPrevious() }); cfg3.Metadata![JetStreamVersioning.JsRequiredLevelMetadataKey].ShouldBe("previous-level"); // copy-previous: key from prevCfg is copied into cfg var cfg4 = new StreamConfig { Metadata = MetadataAtLevel("-1") }; JetStreamVersioning.CopyStreamMetadata(cfg4, new StreamConfig { Metadata = MetadataPrevious() }); cfg4.Metadata![JetStreamVersioning.JsRequiredLevelMetadataKey].ShouldBe("previous-level"); // delete-missing-fields: prevCfg has empty metadata dict → key absent in cfg var cfg5 = new StreamConfig { Metadata = MetadataAtLevel("-1") }; JetStreamVersioning.CopyStreamMetadata(cfg5, new StreamConfig { Metadata = new Dictionary() }); (cfg5.Metadata?.ContainsKey(JetStreamVersioning.JsRequiredLevelMetadataKey) ?? false).ShouldBeFalse(); } // ------------------------------------------------------------------------- // T:1796 — TestJetStreamCopyStreamMetadataRemoveDynamicFields // ------------------------------------------------------------------------- [Fact] // T:1796 public void CopyStreamMetadata_RemovesDynamicFields() { // Copy from null prevCfg — dynamic fields should be removed and key absent. var cfg = new StreamConfig { Metadata = new Dictionary { [JetStreamVersioning.JsServerVersionMetadataKey] = "dynamic-version", [JetStreamVersioning.JsServerLevelMetadataKey] = "dynamic-level", } }; JetStreamVersioning.CopyStreamMetadata(cfg, null); cfg.Metadata.ShouldBeNull(); // all entries removed → null'd // Copy from prevCfg with req-level → dynamic fields removed, req-level preserved. var cfg2 = new StreamConfig { Metadata = new Dictionary { [JetStreamVersioning.JsServerVersionMetadataKey] = "dynamic-version", [JetStreamVersioning.JsServerLevelMetadataKey] = "dynamic-level", } }; var prev = new StreamConfig { Metadata = MetadataAtLevel("0") }; JetStreamVersioning.CopyStreamMetadata(cfg2, prev); cfg2.Metadata.ShouldNotBeNull(); cfg2.Metadata!.ShouldNotContainKey(JetStreamVersioning.JsServerVersionMetadataKey); cfg2.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerLevelMetadataKey); cfg2.Metadata[JetStreamVersioning.JsRequiredLevelMetadataKey].ShouldBe("0"); } // ------------------------------------------------------------------------- // T:1797 — TestJetStreamSetStaticConsumerMetadata // ------------------------------------------------------------------------- [Fact] // T:1797 public void SetStaticConsumerMetadata_VariousConfigs_SetsCorrectApiLevel() { var pauseUntil = new DateTime(1970, 1, 1, 0, 0, 1, DateTimeKind.Utc); // Unix(0, 0) = epoch+1s var pauseUntilZero = default(DateTime); var cases = new[] { ("empty", new ConsumerConfig(), "0"), ("overwrite-user-provided", new ConsumerConfig { Metadata = MetadataPrevious() }, "0"), ("PauseUntil/zero", new ConsumerConfig { PauseUntil = pauseUntilZero }, "0"), ("PauseUntil", new ConsumerConfig { PauseUntil = pauseUntil }, "1"), ("Pinned", new ConsumerConfig { PriorityPolicy = PriorityPolicy.PriorityPinnedClient, PriorityGroups = new[] { "a" } }, "1"), }; foreach (var (desc, cfg, expectedLevel) in cases) { JetStreamVersioning.SetStaticConsumerMetadata(cfg); var level = cfg.Metadata![JetStreamVersioning.JsRequiredLevelMetadataKey]; level.ShouldBe(expectedLevel, $"case: {desc}"); int.Parse(level).ShouldBeLessThanOrEqualTo(JetStreamVersioning.JsApiLevel, customMessage: $"case: {desc}"); } } // ------------------------------------------------------------------------- // T:1798 — TestJetStreamSetStaticConsumerMetadataRemoveDynamicFields // ------------------------------------------------------------------------- [Fact] // T:1798 public void SetStaticConsumerMetadata_RemovesDynamicFields() { var cfg = new ConsumerConfig { Metadata = new Dictionary { [JetStreamVersioning.JsServerVersionMetadataKey] = "dynamic-version", [JetStreamVersioning.JsServerLevelMetadataKey] = "dynamic-level", } }; JetStreamVersioning.SetStaticConsumerMetadata(cfg); cfg.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerVersionMetadataKey); cfg.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerLevelMetadataKey); cfg.Metadata[JetStreamVersioning.JsRequiredLevelMetadataKey].ShouldBe("0"); } // ------------------------------------------------------------------------- // T:1799 — TestJetStreamSetDynamicConsumerMetadata // ------------------------------------------------------------------------- [Fact] // T:1799 public void SetDynamicConsumerMetadata_DoesNotMutateOriginal_AddsVersionFields() { var cfg = new ConsumerConfig { Metadata = MetadataAtLevel("0") }; var newCfg = JetStreamVersioning.SetDynamicConsumerMetadata(cfg); // Original must NOT have dynamic fields. cfg.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerVersionMetadataKey); cfg.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerLevelMetadataKey); // New copy must have dynamic fields. newCfg.Metadata![JetStreamVersioning.JsRequiredLevelMetadataKey].ShouldBe("0"); newCfg.Metadata[JetStreamVersioning.JsServerVersionMetadataKey].ShouldBe(ServerConstants.Version); newCfg.Metadata[JetStreamVersioning.JsServerLevelMetadataKey] .ShouldBe(JetStreamVersioning.JsApiLevel.ToString()); } // ------------------------------------------------------------------------- // T:1800 — TestJetStreamSetDynamicConsumerInfoMetadata // ------------------------------------------------------------------------- [Fact] // T:1800 public void SetDynamicConsumerInfoMetadata_DoesNotMutateOriginal_AddsVersionFields() { var ci = new ConsumerInfo { Config = new ConsumerConfig { Metadata = MetadataAtLevel("0") } }; var newCi = JetStreamVersioning.SetDynamicConsumerInfoMetadata(ci); // Configs must not be reference-equal (we got a new object). ReferenceEquals(ci, newCi).ShouldBeFalse(); // Original config must NOT have dynamic fields. ci.Config!.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerVersionMetadataKey); ci.Config.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerLevelMetadataKey); // New config must have dynamic fields. newCi.Config!.Metadata![JetStreamVersioning.JsRequiredLevelMetadataKey].ShouldBe("0"); newCi.Config.Metadata[JetStreamVersioning.JsServerVersionMetadataKey].ShouldBe(ServerConstants.Version); newCi.Config.Metadata[JetStreamVersioning.JsServerLevelMetadataKey] .ShouldBe(JetStreamVersioning.JsApiLevel.ToString()); } // ------------------------------------------------------------------------- // T:1801 — TestJetStreamCopyConsumerMetadata // ------------------------------------------------------------------------- [Fact] // T:1801 public void CopyConsumerMetadata_VariousScenarios_CopiesRequiredLevelKey() { // no-previous-ignore var cfg1 = new ConsumerConfig { Metadata = MetadataAtLevel("-1") }; JetStreamVersioning.CopyConsumerMetadata(cfg1, null); (cfg1.Metadata?.ContainsKey(JetStreamVersioning.JsRequiredLevelMetadataKey) ?? false).ShouldBeFalse(); // nil-previous-metadata-ignore var cfg2 = new ConsumerConfig { Metadata = MetadataAtLevel("-1") }; JetStreamVersioning.CopyConsumerMetadata(cfg2, new ConsumerConfig { Metadata = null }); (cfg2.Metadata?.ContainsKey(JetStreamVersioning.JsRequiredLevelMetadataKey) ?? false).ShouldBeFalse(); // nil-current-metadata-ignore var cfg3 = new ConsumerConfig { Metadata = null }; JetStreamVersioning.CopyConsumerMetadata(cfg3, new ConsumerConfig { Metadata = MetadataPrevious() }); cfg3.Metadata![JetStreamVersioning.JsRequiredLevelMetadataKey].ShouldBe("previous-level"); // copy-previous var cfg4 = new ConsumerConfig { Metadata = MetadataAtLevel("-1") }; JetStreamVersioning.CopyConsumerMetadata(cfg4, new ConsumerConfig { Metadata = MetadataPrevious() }); cfg4.Metadata![JetStreamVersioning.JsRequiredLevelMetadataKey].ShouldBe("previous-level"); // delete-missing-fields var cfg5 = new ConsumerConfig { Metadata = MetadataAtLevel("-1") }; JetStreamVersioning.CopyConsumerMetadata(cfg5, new ConsumerConfig { Metadata = new Dictionary() }); (cfg5.Metadata?.ContainsKey(JetStreamVersioning.JsRequiredLevelMetadataKey) ?? false).ShouldBeFalse(); } // ------------------------------------------------------------------------- // T:1802 — TestJetStreamCopyConsumerMetadataRemoveDynamicFields // ------------------------------------------------------------------------- [Fact] // T:1802 public void CopyConsumerMetadata_RemovesDynamicFields() { // Copy from null prevCfg → dynamic removed, key absent. var cfg = new ConsumerConfig { Metadata = new Dictionary { [JetStreamVersioning.JsServerVersionMetadataKey] = "dynamic-version", [JetStreamVersioning.JsServerLevelMetadataKey] = "dynamic-level", } }; JetStreamVersioning.CopyConsumerMetadata(cfg, null); cfg.Metadata.ShouldBeNull(); // Copy from prevCfg with req-level → dynamic removed, req-level preserved. var cfg2 = new ConsumerConfig { Metadata = new Dictionary { [JetStreamVersioning.JsServerVersionMetadataKey] = "dynamic-version", [JetStreamVersioning.JsServerLevelMetadataKey] = "dynamic-level", } }; var prev = new ConsumerConfig { Metadata = MetadataAtLevel("0") }; JetStreamVersioning.CopyConsumerMetadata(cfg2, prev); cfg2.Metadata.ShouldNotBeNull(); cfg2.Metadata!.ShouldNotContainKey(JetStreamVersioning.JsServerVersionMetadataKey); cfg2.Metadata.ShouldNotContainKey(JetStreamVersioning.JsServerLevelMetadataKey); cfg2.Metadata[JetStreamVersioning.JsRequiredLevelMetadataKey].ShouldBe("0"); } // ------------------------------------------------------------------------- // T:1803 — TestJetStreamMetadataMutations — deferred: requires RunBasicJetStreamServer // ------------------------------------------------------------------------- [Fact(Skip = "deferred: requires running JetStream server")] // T:1803 public void JetStreamMetadataMutations_RequiresRunningServer() { } // ------------------------------------------------------------------------- // T:1804 — TestJetStreamMetadataStreamRestoreAndRestart — deferred // ------------------------------------------------------------------------- [Fact(Skip = "deferred: requires running JetStream server")] // T:1804 public void JetStreamMetadataStreamRestoreAndRestart_RequiresRunningServer() { } // ------------------------------------------------------------------------- // T:1805 — TestJetStreamMetadataStreamRestoreAndRestartCluster — deferred // ------------------------------------------------------------------------- [Fact(Skip = "deferred: requires running JetStream cluster")] // T:1805 public void JetStreamMetadataStreamRestoreAndRestartCluster_RequiresRunningServer() { } // ------------------------------------------------------------------------- // T:1806 — TestJetStreamApiErrorOnRequiredApiLevel — deferred // ------------------------------------------------------------------------- [Fact(Skip = "deferred: requires running JetStream server")] // T:1806 public void JetStreamApiErrorOnRequiredApiLevel_RequiresRunningServer() { } // ------------------------------------------------------------------------- // T:1807 — TestJetStreamApiErrorOnRequiredApiLevelDirectGet — deferred // ------------------------------------------------------------------------- [Fact(Skip = "deferred: requires running JetStream server")] // T:1807 public void JetStreamApiErrorOnRequiredApiLevelDirectGet_RequiresRunningServer() { } // ------------------------------------------------------------------------- // T:1808 — TestJetStreamApiErrorOnRequiredApiLevelPullConsumerNextMsg — deferred // ------------------------------------------------------------------------- [Fact(Skip = "deferred: requires running JetStream server")] // T:1808 public void JetStreamApiErrorOnRequiredApiLevelPullConsumerNextMsg_RequiresRunningServer() { } }