feat: add account-scoped filtering to /connz (Gap 10.2)

Add ConnzConnectionInfo, ConnzFilterResult, ConnzFilterOptions, and
ConnzFilter static class to Connz.cs, providing a pure unit-testable
layer for account-scoped filtering and pagination that mirrors the Go
server's /connz ?acc= query-parameter behaviour.  Ten new tests in
ConnzAccountFilterTests.cs cover FilterByAccount (match, no-match,
case-insensitive), ConnzFilterOptions.Parse (acc param, defaults,
offset/limit), and ApplyFilters (account filter, offset, limit,
no-filter pass-through).
This commit is contained in:
Joseph Doherty
2026-02-25 13:04:20 -05:00
parent 68b8a0cee5
commit eb801cd4cf
2 changed files with 327 additions and 0 deletions

View File

@@ -163,6 +163,143 @@ public sealed class SubDetail
public ulong Cid { get; set; }
}
/// <summary>
/// Lightweight connection info record used by the account-scoped filter API.
/// Decoupled from <see cref="ConnInfo"/> so filter logic can be tested without a running server.
/// Go reference: monitor.go ConnInfo (subset of fields relevant to account filtering).
/// </summary>
public sealed record ConnzConnectionInfo(
ulong ClientId,
string RemoteAddress,
string? AccountName,
string? Name,
DateTime ConnectedAt,
long InMsgs,
long OutMsgs,
long InBytes,
long OutBytes);
/// <summary>
/// Paginated result returned by <see cref="ConnzFilter.ApplyFilters"/>.
/// </summary>
public sealed record ConnzFilterResult(
IReadOnlyList<ConnzConnectionInfo> Connections,
int Total,
int Offset,
int Limit);
/// <summary>
/// Query-string options for the account-scoped filter API.
/// Parses the ?acc=, ?state=, ?offset=, and ?limit= parameters that the Go server
/// accepts on the /connz endpoint.
/// Go reference: monitor.go ConnzOptions / Connz().
/// </summary>
public sealed class ConnzFilterOptions
{
public string? AccountFilter { get; init; }
/// <summary>"open", "closed", or "any" (default: "open")</summary>
public string? StateFilter { get; init; }
public int Offset { get; init; }
public int Limit { get; init; } = 1024;
/// <summary>
/// Parses a raw query string (e.g. "?acc=ACCOUNT&amp;state=open&amp;offset=0&amp;limit=100")
/// into a <see cref="ConnzFilterOptions"/> instance.
/// </summary>
public static ConnzFilterOptions Parse(string? queryString)
{
if (string.IsNullOrEmpty(queryString))
return new ConnzFilterOptions();
// Strip leading '?'
var qs = queryString.TrimStart('?');
string? accountFilter = null;
string? stateFilter = null;
int offset = 0;
int limit = 1024;
foreach (var pair in qs.Split('&', StringSplitOptions.RemoveEmptyEntries))
{
var eqIdx = pair.IndexOf('=');
if (eqIdx < 0) continue;
var key = Uri.UnescapeDataString(pair[..eqIdx]).ToLowerInvariant();
var value = Uri.UnescapeDataString(pair[(eqIdx + 1)..]);
switch (key)
{
case "acc":
accountFilter = value;
break;
case "state":
stateFilter = value.ToLowerInvariant();
break;
case "offset" when int.TryParse(value, out var o):
offset = o;
break;
case "limit" when int.TryParse(value, out var l):
limit = l;
break;
}
}
return new ConnzFilterOptions
{
AccountFilter = accountFilter,
StateFilter = stateFilter,
Offset = offset,
Limit = limit,
};
}
}
/// <summary>
/// Pure filtering helper for <see cref="ConnzConnectionInfo"/> collections.
/// Provides account-scoped filtering and pagination that mirror the Go server's
/// /connz ?acc= behavior.
/// Go reference: monitor.go Connz() — account filter branch.
/// </summary>
public static class ConnzFilter
{
/// <summary>
/// Filters <paramref name="connections"/> to only those whose
/// <see cref="ConnzConnectionInfo.AccountName"/> matches <paramref name="accountName"/>
/// using a case-insensitive ordinal comparison.
/// </summary>
public static IReadOnlyList<ConnzConnectionInfo> FilterByAccount(
IEnumerable<ConnzConnectionInfo> connections,
string accountName)
{
return connections
.Where(c => string.Equals(c.AccountName, accountName, StringComparison.OrdinalIgnoreCase))
.ToList();
}
/// <summary>
/// Applies all filters specified in <paramref name="options"/> and returns a paginated result.
/// </summary>
public static ConnzFilterResult ApplyFilters(
IEnumerable<ConnzConnectionInfo> connections,
ConnzFilterOptions options)
{
IEnumerable<ConnzConnectionInfo> filtered = connections;
if (!string.IsNullOrEmpty(options.AccountFilter))
filtered = filtered.Where(c =>
string.Equals(c.AccountName, options.AccountFilter, StringComparison.OrdinalIgnoreCase));
var list = filtered.ToList();
var total = list.Count;
var paged = list.Skip(options.Offset).Take(options.Limit).ToList();
return new ConnzFilterResult(paged, total, options.Offset, options.Limit);
}
}
/// <summary>
/// Sort options for connection listing.
/// Corresponds to Go server/monitor_sort_opts.go SortOpt type.