Fix E2E test gaps and add comprehensive E2E + parity test suites
- Fix pull consumer fetch: send original stream subject in HMSG (not inbox) so NATS client distinguishes data messages from control messages - Fix MaxAge expiry: add background timer in StreamManager for periodic pruning - Fix JetStream wire format: Go-compatible anonymous objects with string enums, proper offset-based pagination for stream/consumer list APIs - Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream) - Add ~1000 parity tests across all subsystems (gaps closure) - Update gap inventory docs to reflect implementation status
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
using NATS.Server.JetStream.Api;
|
||||
using NATS.Server.JetStream.Models;
|
||||
using NATS.Server.JetStream.Validation;
|
||||
using NATS.Server.JetStream;
|
||||
|
||||
namespace NATS.Server.Tests.JetStream.Api;
|
||||
|
||||
public class JetStreamApiLimitsParityBatch1Tests
|
||||
{
|
||||
[Fact]
|
||||
public void Constants_match_go_reference_values()
|
||||
{
|
||||
JetStreamApiLimits.JSMaxDescriptionLen.ShouldBe(4_096);
|
||||
JetStreamApiLimits.JSMaxMetadataLen.ShouldBe(128 * 1024);
|
||||
JetStreamApiLimits.JSMaxNameLen.ShouldBe(255);
|
||||
JetStreamApiLimits.JSDefaultRequestQueueLimit.ShouldBe(10_000);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, false)]
|
||||
[InlineData("", false)]
|
||||
[InlineData(" ", false)]
|
||||
[InlineData("ORDERS", true)]
|
||||
[InlineData("ORD ERS", false)]
|
||||
[InlineData("ORDERS.*", false)]
|
||||
[InlineData("ORDERS.>", false)]
|
||||
public void IsValidName_enforces_expected_rules(string? name, bool expected)
|
||||
{
|
||||
JetStreamConfigValidator.IsValidName(name).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stream_create_rejects_name_over_max_length()
|
||||
{
|
||||
var manager = new StreamManager();
|
||||
var response = manager.CreateOrUpdate(new StreamConfig
|
||||
{
|
||||
Name = new string('S', JetStreamApiLimits.JSMaxNameLen + 1),
|
||||
Subjects = ["a"],
|
||||
});
|
||||
|
||||
response.Error.ShouldNotBeNull();
|
||||
response.Error!.Description.ShouldBe("invalid stream name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stream_create_rejects_description_over_max_bytes()
|
||||
{
|
||||
var manager = new StreamManager();
|
||||
var response = manager.CreateOrUpdate(new StreamConfig
|
||||
{
|
||||
Name = "LIMITDESC",
|
||||
Subjects = ["a"],
|
||||
Description = new string('d', JetStreamApiLimits.JSMaxDescriptionLen + 1),
|
||||
});
|
||||
|
||||
response.Error.ShouldNotBeNull();
|
||||
response.Error!.Description.ShouldBe("stream description is too long");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stream_create_rejects_metadata_over_max_bytes()
|
||||
{
|
||||
var manager = new StreamManager();
|
||||
var response = manager.CreateOrUpdate(new StreamConfig
|
||||
{
|
||||
Name = "LIMITMETA",
|
||||
Subjects = ["a"],
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["k"] = new string('m', JetStreamApiLimits.JSMaxMetadataLen),
|
||||
},
|
||||
});
|
||||
|
||||
response.Error.ShouldNotBeNull();
|
||||
response.Error!.Description.ShouldBe("stream metadata exceeds maximum size");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Consumer_create_rejects_durable_name_over_max_length()
|
||||
{
|
||||
var manager = new ConsumerManager();
|
||||
var response = manager.CreateOrUpdate("S", new ConsumerConfig
|
||||
{
|
||||
DurableName = new string('C', JetStreamApiLimits.JSMaxNameLen + 1),
|
||||
});
|
||||
|
||||
response.Error.ShouldNotBeNull();
|
||||
response.Error!.Description.ShouldBe("invalid durable name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Consumer_create_rejects_metadata_over_max_bytes()
|
||||
{
|
||||
var manager = new ConsumerManager();
|
||||
var response = manager.CreateOrUpdate("S", new ConsumerConfig
|
||||
{
|
||||
DurableName = "C1",
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["k"] = new string('m', JetStreamApiLimits.JSMaxMetadataLen),
|
||||
},
|
||||
});
|
||||
|
||||
response.Error.ShouldNotBeNull();
|
||||
response.Error!.Description.ShouldBe("consumer metadata exceeds maximum size");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using NATS.Server.Configuration;
|
||||
using NATS.Server.JetStream;
|
||||
|
||||
namespace NATS.Server.Tests.JetStream;
|
||||
|
||||
public class JetStreamConfigModelParityBatch3Tests
|
||||
{
|
||||
[Fact]
|
||||
public void JetStreamOptions_exposes_extended_go_config_fields()
|
||||
{
|
||||
var opts = new JetStreamOptions
|
||||
{
|
||||
SyncInterval = TimeSpan.FromSeconds(2),
|
||||
SyncAlways = true,
|
||||
CompressOk = true,
|
||||
UniqueTag = "az",
|
||||
Strict = true,
|
||||
MaxAckPending = 123,
|
||||
MemoryMaxStreamBytes = 1111,
|
||||
StoreMaxStreamBytes = 2222,
|
||||
MaxBytesRequired = true,
|
||||
};
|
||||
|
||||
opts.SyncInterval.ShouldBe(TimeSpan.FromSeconds(2));
|
||||
opts.SyncAlways.ShouldBeTrue();
|
||||
opts.CompressOk.ShouldBeTrue();
|
||||
opts.UniqueTag.ShouldBe("az");
|
||||
opts.Strict.ShouldBeTrue();
|
||||
opts.MaxAckPending.ShouldBe(123);
|
||||
opts.MemoryMaxStreamBytes.ShouldBe(1111);
|
||||
opts.StoreMaxStreamBytes.ShouldBe(2222);
|
||||
opts.MaxBytesRequired.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigProcessor_parses_extended_jetstream_fields()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("""
|
||||
jetstream {
|
||||
store_dir: '/tmp/js'
|
||||
max_mem_store: 1024
|
||||
max_file_store: 2048
|
||||
domain: 'D'
|
||||
sync_interval: '2s'
|
||||
sync_always: true
|
||||
compress_ok: true
|
||||
unique_tag: 'az'
|
||||
strict: true
|
||||
max_ack_pending: 42
|
||||
memory_max_stream_bytes: 10000
|
||||
store_max_stream_bytes: 20000
|
||||
max_bytes_required: true
|
||||
}
|
||||
""");
|
||||
|
||||
opts.JetStream.ShouldNotBeNull();
|
||||
var js = opts.JetStream!;
|
||||
js.StoreDir.ShouldBe("/tmp/js");
|
||||
js.MaxMemoryStore.ShouldBe(1024);
|
||||
js.MaxFileStore.ShouldBe(2048);
|
||||
js.Domain.ShouldBe("D");
|
||||
js.SyncInterval.ShouldBe(TimeSpan.FromSeconds(2));
|
||||
js.SyncAlways.ShouldBeTrue();
|
||||
js.CompressOk.ShouldBeTrue();
|
||||
js.UniqueTag.ShouldBe("az");
|
||||
js.Strict.ShouldBeTrue();
|
||||
js.MaxAckPending.ShouldBe(42);
|
||||
js.MemoryMaxStreamBytes.ShouldBe(10000);
|
||||
js.StoreMaxStreamBytes.ShouldBe(20000);
|
||||
js.MaxBytesRequired.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStream_struct_models_cover_stats_limits_and_tiers()
|
||||
{
|
||||
var api = new JetStreamApiStats
|
||||
{
|
||||
Total = 10,
|
||||
Errors = 2,
|
||||
Inflight = 1,
|
||||
};
|
||||
|
||||
var tier = new JetStreamTier
|
||||
{
|
||||
Name = "R3",
|
||||
Memory = 1000,
|
||||
Store = 2000,
|
||||
Streams = 3,
|
||||
Consumers = 5,
|
||||
};
|
||||
|
||||
var limits = new JetStreamAccountLimits
|
||||
{
|
||||
MaxMemory = 10_000,
|
||||
MaxStore = 20_000,
|
||||
MaxStreams = 7,
|
||||
MaxConsumers = 9,
|
||||
MaxAckPending = 25,
|
||||
MemoryMaxStreamBytes = 1_000,
|
||||
StoreMaxStreamBytes = 2_000,
|
||||
MaxBytesRequired = true,
|
||||
Tiers = new Dictionary<string, JetStreamTier>
|
||||
{
|
||||
["R3"] = tier,
|
||||
},
|
||||
};
|
||||
|
||||
var stats = new JetStreamStats
|
||||
{
|
||||
Memory = 123,
|
||||
Store = 456,
|
||||
ReservedMemory = 11,
|
||||
ReservedStore = 22,
|
||||
Accounts = 2,
|
||||
HaAssets = 4,
|
||||
Api = api,
|
||||
};
|
||||
|
||||
limits.Tiers["R3"].Name.ShouldBe("R3");
|
||||
limits.MaxAckPending.ShouldBe(25);
|
||||
limits.MaxBytesRequired.ShouldBeTrue();
|
||||
|
||||
stats.Memory.ShouldBe(123);
|
||||
stats.Store.ShouldBe(456);
|
||||
stats.Api.Total.ShouldBe(10UL);
|
||||
stats.Api.Errors.ShouldBe(2UL);
|
||||
stats.Api.Inflight.ShouldBe(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server.Configuration;
|
||||
|
||||
namespace NATS.Server.Tests.JetStream;
|
||||
|
||||
public class JetStreamServerConfigParityBatch2Tests
|
||||
{
|
||||
[Fact]
|
||||
public void JetStream_constants_match_go_default_values()
|
||||
{
|
||||
JetStreamOptions.JetStreamStoreDir.ShouldBe("jetstream");
|
||||
JetStreamOptions.JetStreamMaxStoreDefault.ShouldBe(1L << 40);
|
||||
JetStreamOptions.JetStreamMaxMemDefault.ShouldBe(256L * 1024 * 1024);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Server_exposes_jetstream_enabled_config_and_store_dir()
|
||||
{
|
||||
var options = new NatsOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
JetStream = new JetStreamOptions
|
||||
{
|
||||
StoreDir = Path.Combine(Path.GetTempPath(), "js-" + Guid.NewGuid().ToString("N")),
|
||||
MaxMemoryStore = 10_000,
|
||||
MaxFileStore = 20_000,
|
||||
MaxStreams = 7,
|
||||
MaxConsumers = 11,
|
||||
Domain = "D1",
|
||||
},
|
||||
};
|
||||
|
||||
var server = new NatsServer(options, NullLoggerFactory.Instance);
|
||||
using var cts = new CancellationTokenSource();
|
||||
_ = server.StartAsync(cts.Token);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
try
|
||||
{
|
||||
server.JetStreamEnabled().ShouldBeTrue();
|
||||
server.StoreDir().ShouldBe(options.JetStream.StoreDir);
|
||||
|
||||
var cfg = server.JetStreamConfig();
|
||||
cfg.ShouldNotBeNull();
|
||||
cfg!.StoreDir.ShouldBe(options.JetStream.StoreDir);
|
||||
cfg.MaxMemoryStore.ShouldBe(options.JetStream.MaxMemoryStore);
|
||||
cfg.MaxFileStore.ShouldBe(options.JetStream.MaxFileStore);
|
||||
cfg.MaxStreams.ShouldBe(options.JetStream.MaxStreams);
|
||||
cfg.MaxConsumers.ShouldBe(options.JetStream.MaxConsumers);
|
||||
cfg.Domain.ShouldBe(options.JetStream.Domain);
|
||||
|
||||
cfg.MaxStreams = 99;
|
||||
server.JetStreamConfig()!.MaxStreams.ShouldBe(options.JetStream.MaxStreams);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
if (Directory.Exists(options.JetStream.StoreDir))
|
||||
Directory.Delete(options.JetStream.StoreDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Server_returns_empty_or_null_jetstream_config_when_disabled()
|
||||
{
|
||||
var server = new NatsServer(new NatsOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
}, NullLoggerFactory.Instance);
|
||||
|
||||
server.JetStreamEnabled().ShouldBeFalse();
|
||||
server.JetStreamConfig().ShouldBeNull();
|
||||
server.StoreDir().ShouldBe(string.Empty);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user