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:
@@ -9,7 +9,9 @@ public sealed class JetStreamApiResponse
|
||||
public JetStreamConsumerInfo? ConsumerInfo { get; init; }
|
||||
public JetStreamAccountInfo? AccountInfo { get; init; }
|
||||
public IReadOnlyList<string>? StreamNames { get; init; }
|
||||
public IReadOnlyList<JetStreamStreamInfo>? StreamInfoList { get; init; }
|
||||
public IReadOnlyList<string>? ConsumerNames { get; init; }
|
||||
public IReadOnlyList<JetStreamConsumerInfo>? ConsumerInfoList { get; init; }
|
||||
public JetStreamStreamMessage? StreamMessage { get; init; }
|
||||
public JetStreamDirectMessage? DirectMessage { get; init; }
|
||||
public JetStreamSnapshot? Snapshot { get; init; }
|
||||
@@ -17,6 +19,17 @@ public sealed class JetStreamApiResponse
|
||||
public bool Success { get; init; }
|
||||
public ulong Purged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total count of all items (before pagination). Used by list responses for offset-based pagination.
|
||||
/// Go reference: jetstream_api.go — ApiPaged struct includes Total, Offset, Limit fields.
|
||||
/// </summary>
|
||||
public int PaginationTotal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Requested offset for pagination. Echoed back to client so it can calculate the next page.
|
||||
/// </summary>
|
||||
public int PaginationOffset { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the consumer is currently paused. Populated by pause/resume API responses.
|
||||
/// Go reference: server/consumer.go jsConsumerPauseResponse.paused field.
|
||||
@@ -29,6 +42,123 @@ public sealed class JetStreamApiResponse
|
||||
/// </summary>
|
||||
public DateTime? PauseUntil { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a wire-format object for JSON serialization matching the Go server's
|
||||
/// flat response structure (e.g., config/state at root level for stream responses,
|
||||
/// not nested under a wrapper property).
|
||||
/// </summary>
|
||||
public object ToWireFormat()
|
||||
{
|
||||
if (StreamInfo != null)
|
||||
{
|
||||
if (Error != null)
|
||||
return new { type = "io.nats.jetstream.api.v1.stream_create_response", error = Error };
|
||||
return new
|
||||
{
|
||||
type = "io.nats.jetstream.api.v1.stream_create_response",
|
||||
config = ToWireConfig(StreamInfo.Config),
|
||||
state = ToWireState(StreamInfo.State),
|
||||
};
|
||||
}
|
||||
|
||||
if (ConsumerInfo != null)
|
||||
{
|
||||
if (Error != null)
|
||||
return new { type = "io.nats.jetstream.api.v1.consumer_create_response", error = Error };
|
||||
return new
|
||||
{
|
||||
type = "io.nats.jetstream.api.v1.consumer_create_response",
|
||||
stream_name = ConsumerInfo.StreamName,
|
||||
name = ConsumerInfo.Name,
|
||||
config = ToWireConsumerConfig(ConsumerInfo.Config),
|
||||
};
|
||||
}
|
||||
|
||||
if (Error != null)
|
||||
return new { error = Error };
|
||||
|
||||
if (StreamInfoList != null)
|
||||
{
|
||||
var wireStreams = StreamInfoList.Select(s => new
|
||||
{
|
||||
config = ToWireConfig(s.Config),
|
||||
state = ToWireState(s.State),
|
||||
}).ToList();
|
||||
return new { total = PaginationTotal, offset = PaginationOffset, limit = wireStreams.Count, streams = wireStreams };
|
||||
}
|
||||
|
||||
if (StreamNames != null)
|
||||
return new { total = PaginationTotal, offset = PaginationOffset, limit = StreamNames.Count, streams = StreamNames };
|
||||
|
||||
if (ConsumerInfoList != null)
|
||||
{
|
||||
var wireConsumers = ConsumerInfoList.Select(c => new
|
||||
{
|
||||
stream_name = c.StreamName,
|
||||
name = c.Name,
|
||||
config = ToWireConsumerConfig(c.Config),
|
||||
}).ToList();
|
||||
return new { total = PaginationTotal, offset = PaginationOffset, limit = wireConsumers.Count, consumers = wireConsumers };
|
||||
}
|
||||
|
||||
if (ConsumerNames != null)
|
||||
return new { total = PaginationTotal, offset = PaginationOffset, limit = ConsumerNames.Count, consumers = ConsumerNames };
|
||||
|
||||
if (Purged > 0 || Success)
|
||||
return new { success = Success, purged = Purged };
|
||||
|
||||
return new { success = Success };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Go-compatible wire format for StreamConfig.
|
||||
/// Only includes fields the Go server sends, with enums as lowercase strings.
|
||||
/// Go reference: server/stream.go StreamConfig JSON marshaling.
|
||||
/// </summary>
|
||||
private static object ToWireConfig(StreamConfig c) => new
|
||||
{
|
||||
name = c.Name,
|
||||
subjects = c.Subjects,
|
||||
retention = c.Retention.ToString().ToLowerInvariant(),
|
||||
max_consumers = c.MaxConsumers,
|
||||
max_msgs = c.MaxMsgs,
|
||||
max_bytes = c.MaxBytes,
|
||||
max_age = c.MaxAge,
|
||||
max_msgs_per_subject = c.MaxMsgsPer,
|
||||
max_msg_size = c.MaxMsgSize,
|
||||
storage = c.Storage.ToString().ToLowerInvariant(),
|
||||
discard = c.Discard.ToString().ToLowerInvariant(),
|
||||
num_replicas = c.Replicas,
|
||||
duplicate_window = (long)c.DuplicateWindowMs * 1_000_000L,
|
||||
sealed_field = c.Sealed,
|
||||
deny_delete = c.DenyDelete,
|
||||
deny_purge = c.DenyPurge,
|
||||
allow_direct = c.AllowDirect,
|
||||
first_seq = c.FirstSeq,
|
||||
};
|
||||
|
||||
private static object ToWireState(ApiStreamState s) => new
|
||||
{
|
||||
messages = s.Messages,
|
||||
bytes = s.Bytes,
|
||||
first_seq = s.FirstSeq,
|
||||
last_seq = s.LastSeq,
|
||||
consumer_count = 0,
|
||||
};
|
||||
|
||||
private static object ToWireConsumerConfig(ConsumerConfig c) => new
|
||||
{
|
||||
durable_name = string.IsNullOrEmpty(c.DurableName) ? null : c.DurableName,
|
||||
name = string.IsNullOrEmpty(c.DurableName) ? null : c.DurableName,
|
||||
deliver_policy = c.DeliverPolicy.ToString().ToLowerInvariant(),
|
||||
ack_policy = c.AckPolicy.ToString().ToLowerInvariant(),
|
||||
replay_policy = c.ReplayPolicy.ToString().ToLowerInvariant(),
|
||||
ack_wait = (long)c.AckWaitMs * 1_000_000L,
|
||||
max_deliver = c.MaxDeliver,
|
||||
max_ack_pending = c.MaxAckPending,
|
||||
filter_subject = c.FilterSubject,
|
||||
};
|
||||
|
||||
public static JetStreamApiResponse NotFound(string subject) => new()
|
||||
{
|
||||
Error = new JetStreamApiError
|
||||
@@ -99,6 +229,8 @@ public sealed class JetStreamStreamInfo
|
||||
|
||||
public sealed class JetStreamConsumerInfo
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
public string? StreamName { get; init; }
|
||||
public required ConsumerConfig Config { get; init; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user