refactor: extract NATS.Server.JetStream.Tests project
Move 225 JetStream-related test files from NATS.Server.Tests into a dedicated NATS.Server.JetStream.Tests project. This includes root-level JetStream*.cs files, storage test files (FileStore, MemStore, StreamStoreContract), and the full JetStream/ subfolder tree (Api, Cluster, Consumers, MirrorSource, Snapshots, Storage, Streams). Updated all namespaces, added InternalsVisibleTo, registered in the solution file, and added the JETSTREAM_INTEGRATION_MATRIX define.
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
using NATS.Server.JetStream.Cluster;
|
||||
|
||||
namespace NATS.Server.JetStream.Tests.JetStream.Cluster;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for MetaSnapshotCodec: encode/decode round-trip, S2 compression, versioning.
|
||||
/// Go reference: jetstream_cluster.go:2075-2145.
|
||||
/// </summary>
|
||||
public class MetaSnapshotCodecTests
|
||||
{
|
||||
[Fact]
|
||||
public void Encode_decode_round_trips()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go encodeMetaSnapshot/decodeMetaSnapshot round-trip
|
||||
var assignments = new Dictionary<string, StreamAssignment>
|
||||
{
|
||||
["stream-A"] = new StreamAssignment
|
||||
{
|
||||
StreamName = "stream-A",
|
||||
Group = new RaftGroup { Name = "rg-a", Peers = ["n1", "n2", "n3"] },
|
||||
ConfigJson = """{"subjects":["foo.>"]}""",
|
||||
},
|
||||
["stream-B"] = new StreamAssignment
|
||||
{
|
||||
StreamName = "stream-B",
|
||||
Group = new RaftGroup { Name = "rg-b", Peers = ["n1", "n2"] },
|
||||
ConfigJson = """{"subjects":["bar.>"]}""",
|
||||
},
|
||||
};
|
||||
|
||||
// Add a consumer to stream-B
|
||||
assignments["stream-B"].Consumers["con-1"] = new ConsumerAssignment
|
||||
{
|
||||
ConsumerName = "con-1",
|
||||
StreamName = "stream-B",
|
||||
Group = new RaftGroup { Name = "rg-c1", Peers = ["n1", "n2"] },
|
||||
};
|
||||
|
||||
var encoded = MetaSnapshotCodec.Encode(assignments);
|
||||
encoded.ShouldNotBeEmpty();
|
||||
|
||||
var decoded = MetaSnapshotCodec.Decode(encoded);
|
||||
decoded.Count.ShouldBe(2);
|
||||
decoded["stream-A"].StreamName.ShouldBe("stream-A");
|
||||
decoded["stream-A"].Group.Peers.Count.ShouldBe(3);
|
||||
decoded["stream-B"].Consumers.Count.ShouldBe(1);
|
||||
decoded["stream-B"].Consumers["con-1"].ConsumerName.ShouldBe("con-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Encoded_snapshot_is_compressed()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go S2 compression of meta snapshots
|
||||
var assignments = new Dictionary<string, StreamAssignment>();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
assignments[$"stream-{i}"] = new StreamAssignment
|
||||
{
|
||||
StreamName = $"stream-{i}",
|
||||
Group = new RaftGroup { Name = $"rg-{i}", Peers = ["n1", "n2", "n3"] },
|
||||
ConfigJson = """{"subjects":["test.>"]}""",
|
||||
};
|
||||
}
|
||||
|
||||
var encoded = MetaSnapshotCodec.Encode(assignments);
|
||||
var json = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(assignments);
|
||||
|
||||
// S2 compressed + 2-byte version header should be smaller than raw JSON
|
||||
encoded.Length.ShouldBeLessThan(json.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_snapshot_round_trips()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go decodeMetaSnapshot handles empty map
|
||||
var empty = new Dictionary<string, StreamAssignment>();
|
||||
var encoded = MetaSnapshotCodec.Encode(empty);
|
||||
var decoded = MetaSnapshotCodec.Decode(encoded);
|
||||
decoded.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Versioned_format_rejects_unknown_version()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go version check in decodeMetaSnapshot
|
||||
var bad = new byte[] { 0xFF, 0xFF, 0, 0 }; // version 65535
|
||||
Should.Throw<InvalidOperationException>(() => MetaSnapshotCodec.Decode(bad));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decode_rejects_too_short_input()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go guard against truncated snapshot
|
||||
Should.Throw<InvalidOperationException>(() => MetaSnapshotCodec.Decode([0x01]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Encoded_snapshot_begins_with_version_one_header()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:2075 — versioned header allows future format evolution
|
||||
var assignments = new Dictionary<string, StreamAssignment>
|
||||
{
|
||||
["s1"] = new StreamAssignment
|
||||
{
|
||||
StreamName = "s1",
|
||||
Group = new RaftGroup { Name = "g1", Peers = ["n1"] },
|
||||
},
|
||||
};
|
||||
|
||||
var encoded = MetaSnapshotCodec.Encode(assignments);
|
||||
|
||||
// Little-endian version 1: bytes [0x01, 0x00]
|
||||
encoded[0].ShouldBe((byte)0x01);
|
||||
encoded[1].ShouldBe((byte)0x00);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Round_trip_preserves_all_stream_assignment_fields()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go streamAssignment struct fields preserved across snapshot
|
||||
var created = new DateTime(2025, 6, 15, 12, 0, 0, DateTimeKind.Utc);
|
||||
var assignments = new Dictionary<string, StreamAssignment>
|
||||
{
|
||||
["my-stream"] = new StreamAssignment
|
||||
{
|
||||
StreamName = "my-stream",
|
||||
Group = new RaftGroup
|
||||
{
|
||||
Name = "rg-main",
|
||||
Peers = ["peer-a", "peer-b", "peer-c"],
|
||||
StorageType = "memory",
|
||||
Cluster = "cluster-east",
|
||||
Preferred = "peer-a",
|
||||
},
|
||||
Created = created,
|
||||
ConfigJson = """{"subjects":["events.>"],"storage":"memory"}""",
|
||||
SyncSubject = "$JS.SYNC.my-stream",
|
||||
Responded = true,
|
||||
Recovering = false,
|
||||
Reassigning = true,
|
||||
},
|
||||
};
|
||||
|
||||
var decoded = MetaSnapshotCodec.Decode(MetaSnapshotCodec.Encode(assignments));
|
||||
|
||||
var sa = decoded["my-stream"];
|
||||
sa.StreamName.ShouldBe("my-stream");
|
||||
sa.Group.Name.ShouldBe("rg-main");
|
||||
sa.Group.Peers.ShouldBe(["peer-a", "peer-b", "peer-c"]);
|
||||
sa.Group.StorageType.ShouldBe("memory");
|
||||
sa.Group.Cluster.ShouldBe("cluster-east");
|
||||
sa.Group.Preferred.ShouldBe("peer-a");
|
||||
sa.Created.ShouldBe(created);
|
||||
sa.ConfigJson.ShouldBe("""{"subjects":["events.>"],"storage":"memory"}""");
|
||||
sa.SyncSubject.ShouldBe("$JS.SYNC.my-stream");
|
||||
sa.Responded.ShouldBeTrue();
|
||||
sa.Recovering.ShouldBeFalse();
|
||||
sa.Reassigning.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Round_trip_preserves_multiple_consumers_per_stream()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go consumerAssignment map restored in snapshot
|
||||
var sa = new StreamAssignment
|
||||
{
|
||||
StreamName = "multi-consumer-stream",
|
||||
Group = new RaftGroup { Name = "rg-mc", Peers = ["n1", "n2", "n3"] },
|
||||
};
|
||||
|
||||
sa.Consumers["consumer-alpha"] = new ConsumerAssignment
|
||||
{
|
||||
ConsumerName = "consumer-alpha",
|
||||
StreamName = "multi-consumer-stream",
|
||||
Group = new RaftGroup { Name = "rg-alpha", Peers = ["n1"] },
|
||||
ConfigJson = """{"deliver_subject":"out.alpha"}""",
|
||||
Responded = true,
|
||||
};
|
||||
sa.Consumers["consumer-beta"] = new ConsumerAssignment
|
||||
{
|
||||
ConsumerName = "consumer-beta",
|
||||
StreamName = "multi-consumer-stream",
|
||||
Group = new RaftGroup { Name = "rg-beta", Peers = ["n2", "n3"] },
|
||||
Recovering = true,
|
||||
};
|
||||
|
||||
var assignments = new Dictionary<string, StreamAssignment> { ["multi-consumer-stream"] = sa };
|
||||
var decoded = MetaSnapshotCodec.Decode(MetaSnapshotCodec.Encode(assignments));
|
||||
|
||||
var dsa = decoded["multi-consumer-stream"];
|
||||
dsa.Consumers.Count.ShouldBe(2);
|
||||
|
||||
var alpha = dsa.Consumers["consumer-alpha"];
|
||||
alpha.ConsumerName.ShouldBe("consumer-alpha");
|
||||
alpha.StreamName.ShouldBe("multi-consumer-stream");
|
||||
alpha.Group.Name.ShouldBe("rg-alpha");
|
||||
alpha.ConfigJson.ShouldBe("""{"deliver_subject":"out.alpha"}""");
|
||||
alpha.Responded.ShouldBeTrue();
|
||||
|
||||
var beta = dsa.Consumers["consumer-beta"];
|
||||
beta.ConsumerName.ShouldBe("consumer-beta");
|
||||
beta.Group.Peers.Count.ShouldBe(2);
|
||||
beta.Recovering.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user