diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs new file mode 100644 index 000000000..3ddec51b2 --- /dev/null +++ b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; + +namespace EventStore.Client { + public partial class EventStoreClientSettings { + /// + /// Creates client settings from a connection string + /// + /// + /// + public static EventStoreClientSettings Create(string connectionString) { + return ConnectionStringParser.Parse(connectionString); + } + + private static class ConnectionStringParser { + private const string SchemeSeparator = "://"; + private const string UserInfoSeparator = "@"; + private const string Colon = ":"; + private const string Slash = "/"; + private const string Comma = ","; + private const string Ampersand = "&"; + private const string Equal = "="; + private const string QuestionMark = "?"; + private static readonly string[] Schemes = { "esdb" }; + private static readonly int DefaultPort = EventStoreClientConnectivitySettings.Default.Address.Port; + private static readonly bool DefaultUseTls = true; + + private static readonly Dictionary SettingsType = new Dictionary (StringComparer.InvariantCultureIgnoreCase) { + {"ConnectionName", typeof(string)}, + {"MaxDiscoverAttempts", typeof(int)}, + {"DiscoveryInterval", typeof(int)}, + {"GossipTimeout", typeof(int)}, + {"NodePreference", typeof(string)}, + {"Tls", typeof(bool)}, + {"TlsVerifyCert", typeof(bool)}, + {"OperationTimeout", typeof(int)}, + {"ThrowOnAppendFailure", typeof(bool)} + }; + + public static EventStoreClientSettings Parse(string connectionString) { + var currentIndex = 0; + var schemeIndex = connectionString.IndexOf(SchemeSeparator, currentIndex, StringComparison.Ordinal); + if (schemeIndex == -1) + throw new NoSchemeException(); + var scheme = ParseScheme(connectionString.Substring(0, schemeIndex)); + + currentIndex = schemeIndex + SchemeSeparator.Length; + var userInfoIndex = connectionString.IndexOf(UserInfoSeparator, currentIndex, StringComparison.Ordinal); + (string user, string pass) userInfo = (null, null); + if (userInfoIndex != -1) { + userInfo = ParseUserInfo(connectionString.Substring(currentIndex, userInfoIndex - currentIndex)); + currentIndex = userInfoIndex + UserInfoSeparator.Length; + } + + + var slashIndex = connectionString.IndexOf(Slash, currentIndex, StringComparison.Ordinal); + var questionMarkIndex = connectionString.IndexOf(QuestionMark, Math.Max(currentIndex, slashIndex), StringComparison.Ordinal); + var endIndex = connectionString.Length; + + //for simpler substring operations: + if (slashIndex == -1) slashIndex = int.MaxValue; + if (questionMarkIndex == -1) questionMarkIndex = int.MaxValue; + + var hostSeparatorIndex = Math.Min(Math.Min(slashIndex, questionMarkIndex), endIndex); + var hosts = ParseHosts(connectionString.Substring(currentIndex,hostSeparatorIndex - currentIndex)); + currentIndex = hostSeparatorIndex; + + string path = ""; + if (slashIndex != int.MaxValue) + path = connectionString.Substring(currentIndex,Math.Min(questionMarkIndex, endIndex) - currentIndex); + + if (path != "" && path != "/") + throw new ConnectionStringParseException($"The specified path must be either an empty string or a forward slash (/) but the following path was found instead: '{path}'"); + + var options = new Dictionary(); + if (questionMarkIndex != int.MaxValue) { + currentIndex = questionMarkIndex + QuestionMark.Length; + options = ParseKeyValuePairs(connectionString.Substring(currentIndex)); + } + + return CreateSettings(scheme, userInfo, hosts, options); + } + + private static EventStoreClientSettings CreateSettings(string scheme, (string user, string pass) userInfo, EndPoint[] hosts, Dictionary options) { + var settings = new EventStoreClientSettings { + ConnectivitySettings = EventStoreClientConnectivitySettings.Default, + OperationOptions = EventStoreClientOperationOptions.Default + }; + + if (userInfo != (null, null)) + settings.DefaultCredentials = new UserCredentials(userInfo.user, userInfo.pass); + + var typedOptions = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (var option in options) { + if (!SettingsType.TryGetValue(option.Key, out var type)) throw new InvalidSettingException($"Unknown option: {option.Key}"); + if(type == typeof(int)){ + if (!int.TryParse(option.Value, out var intValue)) + throw new InvalidSettingException($"{option.Key} must be an integer value"); + typedOptions.Add(option.Key, intValue); + } else if (type == typeof(bool)) { + if (!bool.TryParse(option.Value, out var boolValue)) + throw new InvalidSettingException($"{option.Key} must be either true or false"); + typedOptions.Add(option.Key, boolValue); + } else if (type == typeof(string)) { + typedOptions.Add(option.Key, option.Value); + } + } + + if (typedOptions.TryGetValue("ConnectionName", out object connectionName)) + settings.ConnectionName = (string) connectionName; + + var connSettings = settings.ConnectivitySettings; + + if (typedOptions.TryGetValue("MaxDiscoverAttempts", out object maxDiscoverAttempts)) + connSettings.MaxDiscoverAttempts = (int)maxDiscoverAttempts; + + if (typedOptions.TryGetValue("DiscoveryInterval", out object discoveryInterval)) + connSettings.DiscoveryInterval = TimeSpan.FromMilliseconds((int)discoveryInterval); + + if (typedOptions.TryGetValue("GossipTimeout", out object gossipTimeout)) + connSettings.GossipTimeout = TimeSpan.FromMilliseconds((int)gossipTimeout); + + if (typedOptions.TryGetValue("NodePreference", out object nodePreference)) { + var nodePreferenceLowerCase = ((string)nodePreference).ToLowerInvariant(); + switch (nodePreferenceLowerCase) { + case "leader": + connSettings.NodePreference = NodePreference.Leader; + break; + case "follower": + connSettings.NodePreference = NodePreference.Follower; + break; + case "random": + connSettings.NodePreference = NodePreference.Random; + break; + case "readonlyreplica": + connSettings.NodePreference = NodePreference.ReadOnlyReplica; + break; + default: + throw new InvalidSettingException($"Invalid NodePreference: {nodePreference}"); + } + } + + var useTls = DefaultUseTls; + if(typedOptions.TryGetValue("Tls", out object tls)) { + useTls = (bool)tls; + } + + if (typedOptions.TryGetValue("TlsVerifyCert", out object tlsVerifyCert)) { + if (!(bool)tlsVerifyCert) { + settings.CreateHttpMessageHandler = () => new SocketsHttpHandler { + SslOptions = { + RemoteCertificateValidationCallback = delegate { return true; } + } + }; + } + } + + if(typedOptions.TryGetValue("OperationTimeout", out object operationTimeout)) + settings.OperationOptions.TimeoutAfter = TimeSpan.FromMilliseconds((int) operationTimeout); + + if(typedOptions.TryGetValue("ThrowOnAppendFailure", out object throwOnAppendFailure)) + settings.OperationOptions.ThrowOnAppendFailure = (bool) throwOnAppendFailure; + + if (hosts.Length == 1) { + connSettings.Address = new Uri(hosts[0].ToHttpUrl(useTls?Uri.UriSchemeHttps:Uri.UriSchemeHttp)); + } else { + if (hosts.Any(x => x is DnsEndPoint)) + connSettings.DnsGossipSeeds = hosts.Select(x => new DnsEndPoint(x.GetHost(), x.GetPort())).ToArray(); + else + connSettings.IpGossipSeeds = hosts.Select(x => x as IPEndPoint).ToArray(); + + connSettings.GossipOverHttps = useTls; + } + + return settings; + } + + private static string ParseScheme(string s) { + if (!Schemes.Contains(s)) throw new InvalidSchemeException(s, Schemes); + return s; + } + + private static (string,string) ParseUserInfo(string s) { + var tokens = s.Split(Colon); + if (tokens.Length != 2) throw new InvalidUserCredentialsException(s); + return (tokens[0], tokens[1]); + } + + private static EndPoint[] ParseHosts(string s) { + var hostsTokens = s.Split(Comma); + var hosts = new List(); + foreach (var hostToken in hostsTokens) { + var hostPortToken = hostToken.Split(Colon); + string host; + int port; + switch (hostPortToken.Length) + { + case 1: + host = hostPortToken[0]; + port = DefaultPort; + break; + case 2: + { + host = hostPortToken[0]; + if (!int.TryParse(hostPortToken[1], out port)) + throw new InvalidHostException(hostToken); + break; + } + default: + throw new InvalidHostException(hostToken); + } + + if (host.Length == 0) { + throw new InvalidHostException(hostToken); + } + + if (IPAddress.TryParse(host, out IPAddress ip)) { + hosts.Add(new IPEndPoint(ip, port)); + } else { + hosts.Add(new DnsEndPoint(host, port)); + } + } + + return hosts.ToArray(); + } + + private static Dictionary ParseKeyValuePairs(string s) { + var options = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var optionsTokens = s.Split(Ampersand); + foreach (var optionToken in optionsTokens) { + var (key, val) = ParseKeyValuePair(optionToken); + try { + options.Add(key, val); + } catch (ArgumentException) { + throw new DuplicateKeyException(key); + } + } + return options; + } + + private static (string,string) ParseKeyValuePair(string s) { + var keyValueToken = s.Split(Equal); + if (keyValueToken.Length != 2) { + throw new InvalidKeyValuePairException(s); + } + + return (keyValueToken[0], keyValueToken[1]); + } + } + } +} diff --git a/src/EventStore.Client/EventStoreClientSettings.cs b/src/EventStore.Client/EventStoreClientSettings.cs index d4216a936..feba3f0c6 100644 --- a/src/EventStore.Client/EventStoreClientSettings.cs +++ b/src/EventStore.Client/EventStoreClientSettings.cs @@ -7,7 +7,7 @@ #nullable enable namespace EventStore.Client { - public class EventStoreClientSettings { + public partial class EventStoreClientSettings { public IEnumerable? Interceptors { get; set; } public string? ConnectionName { get; set; } public Func? CreateHttpMessageHandler { get; set; } diff --git a/src/EventStore.Client/AccessDeniedException.cs b/src/EventStore.Client/Exceptions/AccessDeniedException.cs similarity index 100% rename from src/EventStore.Client/AccessDeniedException.cs rename to src/EventStore.Client/Exceptions/AccessDeniedException.cs diff --git a/src/EventStore.Client/Exceptions/ConnectionString/ConnectionStringParseException.cs b/src/EventStore.Client/Exceptions/ConnectionString/ConnectionStringParseException.cs new file mode 100644 index 000000000..843b04eb3 --- /dev/null +++ b/src/EventStore.Client/Exceptions/ConnectionString/ConnectionStringParseException.cs @@ -0,0 +1,7 @@ +using System; + +namespace EventStore.Client { + public class ConnectionStringParseException : Exception { + public ConnectionStringParseException(string message) : base(message) { } + } +} diff --git a/src/EventStore.Client/Exceptions/ConnectionString/DuplicateKeyException.cs b/src/EventStore.Client/Exceptions/ConnectionString/DuplicateKeyException.cs new file mode 100644 index 000000000..283c1ab79 --- /dev/null +++ b/src/EventStore.Client/Exceptions/ConnectionString/DuplicateKeyException.cs @@ -0,0 +1,6 @@ +namespace EventStore.Client { + public class DuplicateKeyException : ConnectionStringParseException { + public DuplicateKeyException(string key) + : base($"Duplicate key: '{key}'") { } + } +} diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidHostException.cs b/src/EventStore.Client/Exceptions/ConnectionString/InvalidHostException.cs new file mode 100644 index 000000000..c905044a8 --- /dev/null +++ b/src/EventStore.Client/Exceptions/ConnectionString/InvalidHostException.cs @@ -0,0 +1,6 @@ +namespace EventStore.Client { + public class InvalidHostException : ConnectionStringParseException { + public InvalidHostException(string host) + : base($"Invalid host: '{host}'") { } + } +} diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidKeyValuePairException.cs b/src/EventStore.Client/Exceptions/ConnectionString/InvalidKeyValuePairException.cs new file mode 100644 index 000000000..93969d380 --- /dev/null +++ b/src/EventStore.Client/Exceptions/ConnectionString/InvalidKeyValuePairException.cs @@ -0,0 +1,6 @@ +namespace EventStore.Client { + public class InvalidKeyValuePairException : ConnectionStringParseException { + public InvalidKeyValuePairException(string keyValuePair) + : base($"Invalid key/value pair: '{keyValuePair}'") { } + } +} diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidSchemeException.cs b/src/EventStore.Client/Exceptions/ConnectionString/InvalidSchemeException.cs new file mode 100644 index 000000000..31465e669 --- /dev/null +++ b/src/EventStore.Client/Exceptions/ConnectionString/InvalidSchemeException.cs @@ -0,0 +1,6 @@ +namespace EventStore.Client { + public class InvalidSchemeException : ConnectionStringParseException { + public InvalidSchemeException(string scheme, string[] supportedSchemes) + : base($"Invalid scheme: '{scheme}'. Supported values are: {string.Join(",", supportedSchemes)}") { } + } +} diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidSettingException.cs b/src/EventStore.Client/Exceptions/ConnectionString/InvalidSettingException.cs new file mode 100644 index 000000000..ed1e4ac7e --- /dev/null +++ b/src/EventStore.Client/Exceptions/ConnectionString/InvalidSettingException.cs @@ -0,0 +1,5 @@ +namespace EventStore.Client { + public class InvalidSettingException : ConnectionStringParseException { + public InvalidSettingException(string message) : base(message) { } + } +} diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidUserCredentialsException.cs b/src/EventStore.Client/Exceptions/ConnectionString/InvalidUserCredentialsException.cs new file mode 100644 index 000000000..1a3df1de4 --- /dev/null +++ b/src/EventStore.Client/Exceptions/ConnectionString/InvalidUserCredentialsException.cs @@ -0,0 +1,6 @@ +namespace EventStore.Client { + public class InvalidUserCredentialsException : ConnectionStringParseException { + public InvalidUserCredentialsException(string userInfo) + : base($"Invalid user credentials: '{userInfo}'. Username & password must be delimited by a colon") { } + } +} diff --git a/src/EventStore.Client/Exceptions/ConnectionString/NoSchemeException.cs b/src/EventStore.Client/Exceptions/ConnectionString/NoSchemeException.cs new file mode 100644 index 000000000..5c2469170 --- /dev/null +++ b/src/EventStore.Client/Exceptions/ConnectionString/NoSchemeException.cs @@ -0,0 +1,6 @@ +namespace EventStore.Client { + public class NoSchemeException : ConnectionStringParseException { + public NoSchemeException() + : base("Could not parse scheme from connection string") { } + } +} diff --git a/src/EventStore.Client/DiscoveryException.cs b/src/EventStore.Client/Exceptions/DiscoveryException.cs similarity index 100% rename from src/EventStore.Client/DiscoveryException.cs rename to src/EventStore.Client/Exceptions/DiscoveryException.cs diff --git a/src/EventStore.Client/NotAuthenticatedException.cs b/src/EventStore.Client/Exceptions/NotAuthenticatedException.cs similarity index 100% rename from src/EventStore.Client/NotAuthenticatedException.cs rename to src/EventStore.Client/Exceptions/NotAuthenticatedException.cs diff --git a/src/EventStore.Client/NotLeaderException.cs b/src/EventStore.Client/Exceptions/NotLeaderException.cs similarity index 100% rename from src/EventStore.Client/NotLeaderException.cs rename to src/EventStore.Client/Exceptions/NotLeaderException.cs diff --git a/src/EventStore.Client/RequiredMetadataPropertyMissingException.cs b/src/EventStore.Client/Exceptions/RequiredMetadataPropertyMissingException.cs similarity index 100% rename from src/EventStore.Client/RequiredMetadataPropertyMissingException.cs rename to src/EventStore.Client/Exceptions/RequiredMetadataPropertyMissingException.cs diff --git a/src/EventStore.Client/ScavengeNotFoundException.cs b/src/EventStore.Client/Exceptions/ScavengeNotFoundException.cs similarity index 100% rename from src/EventStore.Client/ScavengeNotFoundException.cs rename to src/EventStore.Client/Exceptions/ScavengeNotFoundException.cs diff --git a/src/EventStore.Client/StreamDeletedException.cs b/src/EventStore.Client/Exceptions/StreamDeletedException.cs similarity index 100% rename from src/EventStore.Client/StreamDeletedException.cs rename to src/EventStore.Client/Exceptions/StreamDeletedException.cs diff --git a/src/EventStore.Client/StreamNotFoundException.cs b/src/EventStore.Client/Exceptions/StreamNotFoundException.cs similarity index 100% rename from src/EventStore.Client/StreamNotFoundException.cs rename to src/EventStore.Client/Exceptions/StreamNotFoundException.cs diff --git a/src/EventStore.Client/UserNotFoundException.cs b/src/EventStore.Client/Exceptions/UserNotFoundException.cs similarity index 100% rename from src/EventStore.Client/UserNotFoundException.cs rename to src/EventStore.Client/Exceptions/UserNotFoundException.cs diff --git a/src/EventStore.Client/WrongExpectedVersionException.cs b/src/EventStore.Client/Exceptions/WrongExpectedVersionException.cs similarity index 100% rename from src/EventStore.Client/WrongExpectedVersionException.cs rename to src/EventStore.Client/Exceptions/WrongExpectedVersionException.cs diff --git a/test/EventStore.Client.Tests/ConnectionStringTests.cs b/test/EventStore.Client.Tests/ConnectionStringTests.cs new file mode 100644 index 000000000..29a33ca4b --- /dev/null +++ b/test/EventStore.Client.Tests/ConnectionStringTests.cs @@ -0,0 +1,334 @@ +using System; +using System.Net; +using Xunit; + +namespace EventStore.Client { + public class ConnectionStringTests { + [Fact] + public void connection_string_with_no_schema() { + Assert.Throws(() => { + EventStoreClientSettings.Create(":so/mething/random"); + }); + } + + [Fact] + public void connection_string_with_invalid_scheme_should_throw() { + Assert.Throws(() => { + EventStoreClientSettings.Create("esdbwrong://"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("wrong://"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("badesdb://"); + }); + } + + [Fact] + public void connection_string_with_invalid_userinfo_should_throw() { + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://userpass@127.0.0.1/"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pa:ss@127.0.0.1/"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://us:er:pa:ss@127.0.0.1/"); + }); + } + + [Fact] + public void connection_string_with_invalid_host_should_throw() { + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:abc"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:abc/"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:1234,127.0.0.2:abc,127.0.0.3:4321"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:1234,127.0.0.2:abc,127.0.0.3:4321/"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:abc:def"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:abc:def/"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@localhost:1234,127.0.0.2:abc:def,127.0.0.3:4321"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@localhost:1234,127.0.0.2:abc:def,127.0.0.3:4321/"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@localhost:1234,,127.0.0.3:4321"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@localhost:1234,,127.0.0.3:4321/"); + }); + } + + [Fact] + public void connection_string_with_empty_path_after_host_should_not_throw() { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1"); + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:1234"); + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/"); + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1?maxDiscoverAttempts=10"); + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=10"); + } + + [Fact] + public void connection_string_with_non_empty_path_should_throw() { + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/test"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/maxDiscoverAttempts=10"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/hello?maxDiscoverAttempts=10"); + }); + } + + [Fact] + public void connection_string_with_no_key_value_pairs_specified_should_not_throw() { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1"); + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/"); + } + + [Fact] + public void connection_string_with_invalid_key_value_pair_should_throw() { + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=12=34"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts1234"); + }); + } + + [Fact] + public void connection_string_with_duplicate_key_should_throw() { + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=1234&MaxDiscoverAttempts=10"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?gossipTimeout=10&gossipTimeout=30"); + }); + } + + [Fact] + public void connection_string_with_invalid_settings_should_throw() { + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?unknown=1234"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=1234&hello=test"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=abcd"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?discoveryInterval=abcd"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?gossipTimeout=defg"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?tlsVerifyCert=truee"); + }); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=blabla"); + }); + } + + [Fact] + public void with_different_node_preferences() { + Assert.Equal(NodePreference.Leader, EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=leader").ConnectivitySettings.NodePreference); + Assert.Equal(NodePreference.Follower, EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=Follower").ConnectivitySettings.NodePreference); + Assert.Equal(NodePreference.Random, EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=rAndom").ConnectivitySettings.NodePreference); + Assert.Equal(NodePreference.ReadOnlyReplica, EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=ReadOnlyReplica").ConnectivitySettings.NodePreference); + + Assert.Throws(() => { + EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=invalid"); + }); + } + + [Fact] + public void with_valid_single_node_connection_string() { + EventStoreClientSettings settings; + + settings = EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=13&DiscoveryInterval=37&gossipTimeout=33&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false"); + Assert.Equal("user", settings.DefaultCredentials.Username); + Assert.Equal("pass", settings.DefaultCredentials.Password); + Assert.Equal("https://127.0.0.1:2113/",settings.ConnectivitySettings.Address.ToString()); + Assert.Empty(settings.ConnectivitySettings.GossipSeeds); + Assert.Null(settings.ConnectivitySettings.IpGossipSeeds); + Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds); + Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts); + Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); + Assert.Equal(33, settings.ConnectivitySettings.GossipTimeout.TotalMilliseconds); + Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); + Assert.NotNull(settings.CreateHttpMessageHandler); + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1?connectionName=test&maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tls=true&tlsVerifyCert=true&operationTimeout=330&throwOnAppendFailure=faLse"); + Assert.Null(settings.DefaultCredentials); + Assert.Equal("test", settings.ConnectionName); + Assert.Equal("https://127.0.0.1:2113/",settings.ConnectivitySettings.Address.ToString()); + Assert.Empty(settings.ConnectivitySettings.GossipSeeds); + Assert.Null(settings.ConnectivitySettings.IpGossipSeeds); + Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds); + Assert.True(settings.ConnectivitySettings.GossipOverHttps); + Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts); + Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); + Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); + Assert.Null(settings.CreateHttpMessageHandler); + Assert.Equal(330, settings.OperationOptions.TimeoutAfter.Value.TotalMilliseconds); + Assert.False(settings.OperationOptions.ThrowOnAppendFailure); + + settings = EventStoreClientSettings.Create("esdb://hostname:4321/?tls=false"); + Assert.Null(settings.DefaultCredentials); + Assert.Equal("http://hostname:4321/",settings.ConnectivitySettings.Address.ToString()); + Assert.Empty(settings.ConnectivitySettings.GossipSeeds); + Assert.Null(settings.ConnectivitySettings.IpGossipSeeds); + Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds); + Assert.True(settings.ConnectivitySettings.GossipOverHttps); + Assert.Null(settings.CreateHttpMessageHandler); + } + + [Fact] + public void with_default_settings() { + EventStoreClientSettings settings; + settings = EventStoreClientSettings.Create("esdb://hostname:4321/"); + + Assert.Null(settings.ConnectionName); + Assert.Equal(EventStoreClientConnectivitySettings.Default.Address.Scheme, settings.ConnectivitySettings.Address.Scheme); + Assert.Equal(EventStoreClientConnectivitySettings.Default.DiscoveryInterval.TotalMilliseconds, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); + Assert.Null(EventStoreClientConnectivitySettings.Default.DnsGossipSeeds); + Assert.Empty(EventStoreClientConnectivitySettings.Default.GossipSeeds); + Assert.Equal(EventStoreClientConnectivitySettings.Default.GossipTimeout.TotalMilliseconds, settings.ConnectivitySettings.GossipTimeout.TotalMilliseconds); + Assert.Null(EventStoreClientConnectivitySettings.Default.IpGossipSeeds); + Assert.Equal(EventStoreClientConnectivitySettings.Default.MaxDiscoverAttempts, settings.ConnectivitySettings.MaxDiscoverAttempts); + Assert.Equal(EventStoreClientConnectivitySettings.Default.NodePreference, settings.ConnectivitySettings.NodePreference); + Assert.Equal(EventStoreClientConnectivitySettings.Default.GossipOverHttps, settings.ConnectivitySettings.GossipOverHttps); + Assert.Equal(EventStoreClientOperationOptions.Default.TimeoutAfter.Value.TotalMilliseconds, settings.OperationOptions.TimeoutAfter.Value.TotalMilliseconds); + Assert.Equal(EventStoreClientOperationOptions.Default.ThrowOnAppendFailure, settings.OperationOptions.ThrowOnAppendFailure); + } + + [Fact] + public void with_valid_cluster_connection_string() { + EventStoreClientSettings settings; + + settings = EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1,127.0.0.2:3321,127.0.0.3/?maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false"); + Assert.Equal("user", settings.DefaultCredentials.Username); + Assert.Equal("pass", settings.DefaultCredentials.Password); + Assert.NotEmpty(settings.ConnectivitySettings.GossipSeeds); + Assert.NotNull(settings.ConnectivitySettings.IpGossipSeeds); + Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds); + Assert.True(settings.ConnectivitySettings.GossipOverHttps); + Assert.True(settings.ConnectivitySettings.IpGossipSeeds.Length == 3 && + Equals(settings.ConnectivitySettings.IpGossipSeeds[0].Address, IPAddress.Parse("127.0.0.1")) && + Equals(settings.ConnectivitySettings.IpGossipSeeds[0].Port, 2113) && + Equals(settings.ConnectivitySettings.IpGossipSeeds[1].Address, IPAddress.Parse("127.0.0.2")) && + Equals(settings.ConnectivitySettings.IpGossipSeeds[1].Port, 3321) && + Equals(settings.ConnectivitySettings.IpGossipSeeds[2].Address, IPAddress.Parse("127.0.0.3")) && + Equals(settings.ConnectivitySettings.IpGossipSeeds[2].Port, 2113)); + Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts); + Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); + Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); + Assert.NotNull(settings.CreateHttpMessageHandler); + + + settings = EventStoreClientSettings.Create("esdb://user:pass@host1,host2:3321,127.0.0.3/?tls=false&maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false"); + Assert.Equal("user", settings.DefaultCredentials.Username); + Assert.Equal("pass", settings.DefaultCredentials.Password); + Assert.NotEmpty(settings.ConnectivitySettings.GossipSeeds); + Assert.Null(settings.ConnectivitySettings.IpGossipSeeds); + Assert.NotNull(settings.ConnectivitySettings.DnsGossipSeeds); + Assert.False(settings.ConnectivitySettings.GossipOverHttps); + Assert.True(settings.ConnectivitySettings.DnsGossipSeeds.Length == 3 && + Equals(settings.ConnectivitySettings.DnsGossipSeeds[0].Host, "host1") && + Equals(settings.ConnectivitySettings.DnsGossipSeeds[0].Port, 2113) && + Equals(settings.ConnectivitySettings.DnsGossipSeeds[1].Host, "host2") && + Equals(settings.ConnectivitySettings.DnsGossipSeeds[1].Port, 3321) && + Equals(settings.ConnectivitySettings.DnsGossipSeeds[2].Host, "127.0.0.3") && + Equals(settings.ConnectivitySettings.DnsGossipSeeds[2].Port, 2113)); + Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts); + Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); + Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); + Assert.NotNull(settings.CreateHttpMessageHandler); + } + + [Fact] + public void with_different_tls_settings() { + EventStoreClientSettings settings; + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1/"); + Assert.Equal(Uri.UriSchemeHttps, settings.ConnectivitySettings.Address.Scheme); + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1?tls=true"); + Assert.Equal(Uri.UriSchemeHttps, settings.ConnectivitySettings.Address.Scheme); + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1/?tls=FaLsE"); + Assert.Equal(Uri.UriSchemeHttp, settings.ConnectivitySettings.Address.Scheme); + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/"); + Assert.True(settings.ConnectivitySettings.GossipOverHttps); + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3?tls=true"); + Assert.True(settings.ConnectivitySettings.GossipOverHttps); + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/?tls=fAlSe"); + Assert.False(settings.ConnectivitySettings.GossipOverHttps); + } + + [Fact] + public void with_different_tls_verify_cert_settings() { + EventStoreClientSettings settings; + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1/"); + Assert.Null(settings.CreateHttpMessageHandler); + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1/?tlsVerifyCert=TrUe"); + Assert.Null(settings.CreateHttpMessageHandler); + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1/?tlsVerifyCert=FaLsE"); + Assert.NotNull(settings.CreateHttpMessageHandler); + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/"); + Assert.Null(settings.CreateHttpMessageHandler); + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/?tlsVerifyCert=true"); + Assert.Null(settings.CreateHttpMessageHandler); + + settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/?tlsVerifyCert=false"); + Assert.NotNull(settings.CreateHttpMessageHandler); + } + + } +}