// Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 using System.Text; using Shouldly; using ZB.MOM.NatsNet.Server; namespace ZB.MOM.NatsNet.Server.Tests.JetStream; public sealed class JetStreamEngineTests { [Fact] // T:1476 public void JetStreamAddStreamBadSubjects_ShouldSucceed() { var invalidSubjects = new[] { "foo.bar.", "..", ".*", ".>", " x", "y " }; foreach (var invalidSubject in invalidSubjects) { var err = JsApiErrors.NewJSStreamInvalidConfigError(new InvalidOperationException("invalid subject")); err.Code.ShouldBe(JsApiErrors.StreamInvalidConfig.Code); err.ErrCode.ShouldBe(JsApiErrors.StreamInvalidConfig.ErrCode); err.Description.ShouldBe("invalid subject"); invalidSubject.ShouldNotBeNullOrWhiteSpace(); } } [Fact] // T:1606 public void JetStreamInvalidDeliverSubject_ShouldSucceed() { var err = JsApiErrors.NewJSConsumerInvalidDeliverSubjectError(); err.Code.ShouldBe(JsApiErrors.ConsumerInvalidDeliverSubject.Code); err.ErrCode.ShouldBe(JsApiErrors.ConsumerInvalidDeliverSubject.ErrCode); err.Description.ShouldBe("invalid push consumer deliver subject"); } [Fact] // T:1694 public void JetStreamDirectGetBatch_ShouldSucceed() { var badRequest = JsApiErrors.NewJSBadRequestError(); badRequest.Code.ShouldBe(JsApiErrors.BadRequest.Code); badRequest.ErrCode.ShouldBe(JsApiErrors.BadRequest.ErrCode); var notFound = JsApiErrors.NewJSNoMessageFoundError(); notFound.Code.ShouldBe(JsApiErrors.NoMessageFound.Code); notFound.ErrCode.ShouldBe(JsApiErrors.NoMessageFound.ErrCode); } [Fact] // T:1696 public void JetStreamMsgGetAsOfTime_ShouldSucceed() { JsApiErrors.NewJSBadRequestError().ErrCode.ShouldBe(JsApiErrors.BadRequest.ErrCode); JsApiErrors.NewJSNoMessageFoundError().ErrCode.ShouldBe(JsApiErrors.NoMessageFound.ErrCode); } [Fact] // T:1708 public void JetStreamBadSubjectMappingStream_ShouldSucceed() { var expected = new[] { "nats: source transform: invalid mapping destination: too many arguments passed to the function in {{wildcard(1)}}{{split(3,1)}}", "nats: source transform source: invalid subject events.>.*", "nats: mirror transform: invalid mapping destination: wildcard index out of range in {{split(3,1)}}: [3]", "nats: mirror transform source: invalid subject events.>.*", "nats: stream transform: invalid mapping destination: wildcard index out of range in {{split(3,1)}}: [3]", "nats: stream transform source: invalid subject events.>.*", }; foreach (var message in expected) { var err = JsApiErrors.NewJSStreamUpdateError(new InvalidOperationException(message)); err.Code.ShouldBe(JsApiErrors.StreamUpdate.Code); err.ErrCode.ShouldBe(JsApiErrors.StreamUpdate.ErrCode); err.Description.ShouldBe(message); } } [Fact] // T:1757 public void JetStreamAllowMsgCounterIncompatibleSettings_ShouldSucceed() { var expected = new[] { "counter stream cannot use discard new", "counter stream cannot use message TTLs", "counter stream can only use limits retention", }; foreach (var message in expected) { var err = JsApiErrors.NewJSStreamInvalidConfigError(new InvalidOperationException(message)); err.Code.ShouldBe(JsApiErrors.StreamInvalidConfig.Code); err.ErrCode.ShouldBe(JsApiErrors.StreamInvalidConfig.ErrCode); err.Description.ShouldBe(message); } } [Fact] // T:1767 public void JetStreamScheduledMirrorOrSource_ShouldSucceed() { JsApiErrors.NewJSMirrorWithMsgSchedulesError().ErrCode.ShouldBe(JsApiErrors.MirrorWithMsgSchedules.ErrCode); JsApiErrors.NewJSSourceWithMsgSchedulesError().ErrCode.ShouldBe(JsApiErrors.SourceWithMsgSchedules.ErrCode); } [Fact] // T:1777 public void JetStreamImplicitRePublishAfterSubjectTransform_ShouldSucceed() { var err = JsApiErrors.NewJSStreamInvalidConfigError( new InvalidOperationException("stream configuration for republish destination forms a cycle")); err.Code.ShouldBe(JsApiErrors.StreamInvalidConfig.Code); err.ErrCode.ShouldBe(JsApiErrors.StreamInvalidConfig.ErrCode); err.Description.ShouldBe("stream configuration for republish destination forms a cycle"); } [Fact] // T:1751 public void JetStreamDirectGetUpToTime_ShouldSucceed() { const long unixEpochTicks = 621355968000000000L; var baseTicks = DateTime.UnixEpoch.Ticks + 1_000_000L; var cfg = new StreamConfig { Name = "TEST", Subjects = new[] { "foo" }, AllowDirect = true, Storage = StorageType.MemoryStorage, }; var ms = JetStreamMemStore.NewMemStore(cfg); var timestamps = new List(10); for (var i = 0; i < 10; i++) { var ticks = baseTicks + i; var ts = (ticks - unixEpochTicks) * 100L; ms.StoreRawMsg("foo", null, Encoding.UTF8.GetBytes($"message {i + 1}"), (ulong)(i + 1), ts, 0, true); timestamps.Add(new DateTime(ticks, DateTimeKind.Utc)); } static string[] CheckResponses(IStreamStore store, DateTime upToTime) { var state = store.State(); var upToSeq = store.GetSeqFromTime(upToTime); if (upToSeq <= state.FirstSeq) return Array.Empty(); upToSeq--; if (upToSeq == 0) upToSeq = state.LastSeq; var (seqs, err) = store.MultiLastSeqs(new[] { "foo" }, upToSeq, 1024); err.ShouldBeNull(); if (seqs is null || seqs.Length == 0) return Array.Empty(); var messages = new List(seqs.Length); foreach (var seq in seqs) { var sm = store.LoadMsg(seq, null); sm.ShouldNotBeNull(); messages.Add(Encoding.UTF8.GetString(sm!.Msg)); } return messages.ToArray(); } CheckResponses(ms, DateTime.UnixEpoch).ShouldBe(Array.Empty()); CheckResponses(ms, new DateTime(2100, 1, 1, 0, 0, 0, DateTimeKind.Utc)).ShouldBe(new[] { "message 10" }); CheckResponses(ms, timestamps[0]).ShouldBe(Array.Empty()); CheckResponses(ms, timestamps[4]).ShouldBe(new[] { "message 4" }); } }