feat(ldap): LdapEndpoint + FallbackServers entry parser
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
namespace ZB.MOM.WW.Auth.Ldap.Internal;
|
||||
|
||||
/// <summary>A single LDAP server endpoint resolved from configuration.</summary>
|
||||
internal sealed record LdapEndpoint(string Host, int Port)
|
||||
{
|
||||
/// <summary>Renders the endpoint as <c>host:port</c> — the form used in failover log messages.</summary>
|
||||
public override string ToString() => $"{Host}:{Port}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses <c>FallbackServers</c> entries: <c>"host"</c> (adopts the caller's default port)
|
||||
/// or <c>"host:port"</c>. IPv6 literals are not supported — use a DNS name.
|
||||
/// </summary>
|
||||
internal static class LdapEndpointParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to parse a single <c>FallbackServers</c> entry.
|
||||
/// </summary>
|
||||
/// <param name="entry">The configured entry, <c>"host"</c> or <c>"host:port"</c>. Surrounding whitespace is tolerated.</param>
|
||||
/// <param name="defaultPort">Port adopted when the entry carries no explicit one.</param>
|
||||
/// <param name="endpoint">The parsed endpoint on success; undefined otherwise.</param>
|
||||
/// <param name="error">A human-readable reason on failure; empty on success.</param>
|
||||
/// <returns><see langword="true"/> when the entry parsed.</returns>
|
||||
public static bool TryParse(string entry, int defaultPort, out LdapEndpoint endpoint, out string error)
|
||||
{
|
||||
endpoint = null!;
|
||||
error = "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(entry))
|
||||
{
|
||||
error = "entry is empty or whitespace";
|
||||
return false;
|
||||
}
|
||||
|
||||
var parts = entry.Trim().Split(':');
|
||||
if (parts.Length > 2)
|
||||
{
|
||||
error = $"entry \"{entry.Trim()}\" contains more than one ':' " +
|
||||
"(IPv6 literals are not supported — use a DNS name)";
|
||||
return false;
|
||||
}
|
||||
|
||||
var host = parts[0].Trim();
|
||||
if (host.Length == 0)
|
||||
{
|
||||
error = $"entry \"{entry.Trim()}\" has an empty host";
|
||||
return false;
|
||||
}
|
||||
|
||||
var port = defaultPort;
|
||||
if (parts.Length == 2 && (!int.TryParse(parts[1].Trim(), out port) || port is < 1 or > 65535))
|
||||
{
|
||||
error = $"entry \"{entry.Trim()}\" has an invalid port (must be 1–65535)";
|
||||
return false;
|
||||
}
|
||||
|
||||
endpoint = new LdapEndpoint(host, port);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using ZB.MOM.WW.Auth.Ldap.Internal;
|
||||
|
||||
namespace ZB.MOM.WW.Auth.Ldap.Tests;
|
||||
|
||||
public class LdapEndpointParserTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("dc2.example.com", 636, "dc2.example.com", 636)] // bare host -> default port
|
||||
[InlineData("dc2.example.com:3894", 636, "dc2.example.com", 3894)] // explicit port wins
|
||||
[InlineData(" dc2.example.com : 3894 ", 636, "dc2.example.com", 3894)] // whitespace tolerated
|
||||
public void TryParse_ValidEntry_Succeeds(string entry, int defaultPort, string host, int port)
|
||||
{
|
||||
Assert.True(LdapEndpointParser.TryParse(entry, defaultPort, out var ep, out _));
|
||||
Assert.Equal(host, ep.Host);
|
||||
Assert.Equal(port, ep.Port);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")] // empty
|
||||
[InlineData(" ")] // whitespace
|
||||
[InlineData(":636")] // empty host
|
||||
[InlineData("dc2:0")] // port below range
|
||||
[InlineData("dc2:65536")] // port above range
|
||||
[InlineData("dc2:abc")] // non-numeric port
|
||||
[InlineData("2001:db8::1:636")] // IPv6 literal (unsupported, >1 colon)
|
||||
public void TryParse_InvalidEntry_Fails(string entry)
|
||||
{
|
||||
Assert.False(LdapEndpointParser.TryParse(entry, 636, out _, out var error));
|
||||
Assert.NotEmpty(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Endpoint_ToString_IsHostColonPort() =>
|
||||
Assert.Equal("dc2:3894", new LdapEndpoint("dc2", 3894).ToString());
|
||||
}
|
||||
Reference in New Issue
Block a user