diff --git a/src/WebJobs.Extensions.CosmosDB/Bindings/CosmosDBClientBuilder.cs b/src/WebJobs.Extensions.CosmosDB/Bindings/CosmosDBClientBuilder.cs index b1079c6dd..0f9412938 100644 --- a/src/WebJobs.Extensions.CosmosDB/Bindings/CosmosDBClientBuilder.cs +++ b/src/WebJobs.Extensions.CosmosDB/Bindings/CosmosDBClientBuilder.cs @@ -22,9 +22,8 @@ public CosmosClient Convert(CosmosDBAttribute attribute) throw new ArgumentNullException(nameof(attribute)); } - string resolvedConnectionString = _configProvider.ResolveConnectionString(attribute.Connection); return _configProvider.GetService( - connectionString: resolvedConnectionString, + connection: attribute.Connection ?? Constants.DefaultConnectionStringName, preferredLocations: attribute.PreferredLocations); } } diff --git a/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBExtensionConfigProvider.cs b/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBExtensionConfigProvider.cs index 620998a6b..95df58b47 100644 --- a/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBExtensionConfigProvider.cs +++ b/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBExtensionConfigProvider.cs @@ -23,7 +23,6 @@ namespace Microsoft.Azure.WebJobs.Extensions.CosmosDB [Extension("CosmosDB")] internal class CosmosDBExtensionConfigProvider : IExtensionConfigProvider { - private readonly IConfiguration _configuration; private readonly ICosmosDBServiceFactory _cosmosDBServiceFactory; private readonly ICosmosDBSerializerFactory _cosmosSerializerFactory; private readonly INameResolver _nameResolver; @@ -34,11 +33,9 @@ public CosmosDBExtensionConfigProvider( IOptions options, ICosmosDBServiceFactory cosmosDBServiceFactory, ICosmosDBSerializerFactory cosmosSerializerFactory, - IConfiguration configuration, INameResolver nameResolver, ILoggerFactory loggerFactory) { - _configuration = configuration; _cosmosDBServiceFactory = cosmosDBServiceFactory; _cosmosSerializerFactory = cosmosSerializerFactory; _nameResolver = nameResolver; @@ -77,29 +74,19 @@ public void Initialize(ExtensionConfigContext context) // Trigger var rule2 = context.AddBindingRule(); - rule2.BindToTrigger(new CosmosDBTriggerAttributeBindingProviderGenerator(_configuration, _nameResolver, _options, this, _loggerFactory)); + rule2.BindToTrigger(new CosmosDBTriggerAttributeBindingProviderGenerator(_nameResolver, _options, this, _loggerFactory)); } internal void ValidateConnection(CosmosDBAttribute attribute, Type paramType) { - if (string.IsNullOrEmpty(_options.ConnectionString) && - string.IsNullOrEmpty(attribute.Connection)) + if (attribute.Connection == string.Empty) { string attributeProperty = $"{nameof(CosmosDBAttribute)}.{nameof(CosmosDBAttribute.Connection)}"; - string optionsProperty = $"{nameof(CosmosDBOptions)}.{nameof(CosmosDBOptions.ConnectionString)}"; throw new InvalidOperationException( - $"The CosmosDB connection string must be set either via the '{Constants.DefaultConnectionStringName}' IConfiguration connection string, via the {attributeProperty} property or via {optionsProperty}."); + $"The {attributeProperty} property cannot be an empty value."); } } - internal CosmosClient BindForClient(CosmosDBAttribute attribute) - { - string resolvedConnectionString = ResolveConnectionString(attribute.Connection); - return GetService( - connectionString: resolvedConnectionString, - preferredLocations: attribute.PreferredLocations); - } - internal Task BindForItemAsync(CosmosDBAttribute attribute, Type type) { if (string.IsNullOrEmpty(attribute.Id)) @@ -115,31 +102,17 @@ internal Task BindForItemAsync(CosmosDBAttribute attribute, Type t return Task.FromResult(binder); } - internal string ResolveConnectionString(string attributeConnectionString) + internal CosmosClient GetService(string connection, string preferredLocations = "", string userAgent = "") { - // First, try the Attribute's string. - if (!string.IsNullOrEmpty(attributeConnectionString)) - { - return attributeConnectionString; - } - - // Then use the options. - return _options.ConnectionString; - } - - internal CosmosClient GetService(string connectionString, string preferredLocations = "", string userAgent = "") - { - string cacheKey = BuildCacheKey(connectionString, preferredLocations); + string cacheKey = BuildCacheKey(connection, preferredLocations); CosmosClientOptions cosmosClientOptions = CosmosDBUtility.BuildClientOptions(_options.ConnectionMode, _cosmosSerializerFactory.CreateSerializer(), preferredLocations, userAgent); - return ClientCache.GetOrAdd(cacheKey, (c) => _cosmosDBServiceFactory.CreateService(connectionString, cosmosClientOptions)); + return ClientCache.GetOrAdd(cacheKey, (c) => _cosmosDBServiceFactory.CreateService(connection, cosmosClientOptions)); } internal CosmosDBContext CreateContext(CosmosDBAttribute attribute) { - string resolvedConnectionString = ResolveConnectionString(attribute.Connection); - CosmosClient service = GetService( - connectionString: resolvedConnectionString, + connection: attribute.Connection ?? Constants.DefaultConnectionStringName, preferredLocations: attribute.PreferredLocations); return new CosmosDBContext diff --git a/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBOptions.cs b/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBOptions.cs index fd6d28e40..b44d67f39 100644 --- a/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBOptions.cs +++ b/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBOptions.cs @@ -10,11 +10,6 @@ namespace Microsoft.Azure.WebJobs.Extensions.CosmosDB { public class CosmosDBOptions : IOptionsFormatter { - /// - /// Gets or sets the CosmosDB connection string. - /// - public string ConnectionString { get; set; } - /// /// Gets or sets the ConnectionMode used in the CosmosClient instances. /// diff --git a/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBWebJobsBuilderExtensions.cs b/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBWebJobsBuilderExtensions.cs index f97f415b3..bba3ff273 100644 --- a/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBWebJobsBuilderExtensions.cs +++ b/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBWebJobsBuilderExtensions.cs @@ -28,8 +28,6 @@ public static IWebJobsBuilder AddCosmosDB(this IWebJobsBuilder builder) builder.AddExtension() .ConfigureOptions((config, path, options) => { - options.ConnectionString = config.GetConnectionStringOrSetting(Constants.DefaultConnectionStringName); - IConfigurationSection section = config.GetSection(path); section.Bind(options); }); diff --git a/src/WebJobs.Extensions.CosmosDB/Config/DefaultCosmosDBServiceFactory.cs b/src/WebJobs.Extensions.CosmosDB/Config/DefaultCosmosDBServiceFactory.cs index 272403513..3bccc2772 100644 --- a/src/WebJobs.Extensions.CosmosDB/Config/DefaultCosmosDBServiceFactory.cs +++ b/src/WebJobs.Extensions.CosmosDB/Config/DefaultCosmosDBServiceFactory.cs @@ -1,15 +1,92 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System; +using Azure.Core; using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; namespace Microsoft.Azure.WebJobs.Extensions.CosmosDB { internal class DefaultCosmosDBServiceFactory : ICosmosDBServiceFactory { - public CosmosClient CreateService(string connectionString, CosmosClientOptions cosmosClientOptions) + private readonly IConfiguration _configuration; + private readonly AzureComponentFactory _componentFactory; + + public DefaultCosmosDBServiceFactory( + IConfiguration configuration, + AzureComponentFactory componentFactory) + { + this._configuration = configuration; + this._componentFactory = componentFactory; + } + + public CosmosClient CreateService(string connectionName, CosmosClientOptions cosmosClientOptions) + { + CosmosConnectionInformation cosmosConnectionInformation = this.ResolveConnectionInformation(connectionName); + if (cosmosConnectionInformation.UsesConnectionString) + { + // Connection string based auth + return new CosmosClient(cosmosConnectionInformation.ConnectionString, cosmosClientOptions); + } + + // AAD auth + return new CosmosClient(cosmosConnectionInformation.AccountEndpoint, cosmosConnectionInformation.Credential, cosmosClientOptions); + } + + private CosmosConnectionInformation ResolveConnectionInformation(string connection) + { + var connectionSetting = connection ?? Constants.DefaultConnectionStringName; + IConfigurationSection connectionSection = WebJobsConfigurationExtensions.GetWebJobsConnectionStringSection(this._configuration, connectionSetting); + if (!connectionSection.Exists()) + { + // Not found + throw new InvalidOperationException($"Cosmos DB connection configuration '{connectionSetting}' does not exist. " + + $"Make sure that it is a defined App Setting."); + } + + if (!string.IsNullOrWhiteSpace(connectionSection.Value)) + { + return new CosmosConnectionInformation(connectionSection.Value); + } + else + { + string accountEndpoint = connectionSection["accountEndpoint"]; + if (string.IsNullOrWhiteSpace(accountEndpoint)) + { + // Not found + throw new InvalidOperationException($"Connection should have an 'accountEndpoint' property or be a " + + $"string representing a connection string."); + } + + TokenCredential credential = _componentFactory.CreateTokenCredential(connectionSection); + return new CosmosConnectionInformation(accountEndpoint, credential); + } + } + + private class CosmosConnectionInformation { - return new CosmosClient(connectionString, cosmosClientOptions); + public CosmosConnectionInformation(string connectionString) + { + this.ConnectionString = connectionString; + this.UsesConnectionString = true; + } + + public CosmosConnectionInformation(string accountEndpoint, TokenCredential tokenCredential) + { + this.AccountEndpoint = accountEndpoint; + this.Credential = tokenCredential; + this.UsesConnectionString = false; + } + + public bool UsesConnectionString { get; } + + public string ConnectionString { get; } + + public string AccountEndpoint { get; } + + public TokenCredential Credential { get; } } } } diff --git a/src/WebJobs.Extensions.CosmosDB/Trigger/CosmosDBTriggerAttributeBindingProvider.cs b/src/WebJobs.Extensions.CosmosDB/Trigger/CosmosDBTriggerAttributeBindingProvider.cs index aaf5479fc..c6ea9d15b 100644 --- a/src/WebJobs.Extensions.CosmosDB/Trigger/CosmosDBTriggerAttributeBindingProvider.cs +++ b/src/WebJobs.Extensions.CosmosDB/Trigger/CosmosDBTriggerAttributeBindingProvider.cs @@ -8,7 +8,6 @@ using Microsoft.Azure.WebJobs.Host; using Microsoft.Azure.WebJobs.Host.Triggers; using Microsoft.Azure.WebJobs.Logging; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Microsoft.Azure.WebJobs.Extensions.CosmosDB @@ -19,16 +18,14 @@ internal class CosmosDBTriggerAttributeBindingProvider private const string SharedThroughputRequirementException = "Shared throughput collection should have a partition key"; private const string LeaseCollectionRequiredPartitionKey = "/id"; private const string LeaseCollectionRequiredPartitionKeyFromGremlin = "/partitionKey"; - private readonly IConfiguration _configuration; private readonly INameResolver _nameResolver; private readonly CosmosDBOptions _options; private readonly ILogger _logger; private readonly CosmosDBExtensionConfigProvider _configProvider; - public CosmosDBTriggerAttributeBindingProvider(IConfiguration configuration, INameResolver nameResolver, CosmosDBOptions options, + public CosmosDBTriggerAttributeBindingProvider(INameResolver nameResolver, CosmosDBOptions options, CosmosDBExtensionConfigProvider configProvider, ILoggerFactory loggerFactory) { - _configuration = configuration; _nameResolver = nameResolver; _options = options; _configProvider = configProvider; @@ -61,16 +58,16 @@ public async Task TryCreateAsync(TriggerBindingProviderContext try { - string triggerConnectionString = ResolveAttributeConnectionString(attribute); - if (string.IsNullOrEmpty(triggerConnectionString)) + string triggerConnection = ResolveAttributeConnection(attribute); + if (string.IsNullOrEmpty(triggerConnection)) { - throw new InvalidOperationException("The connection string for the monitored container is in an invalid format, please use AccountEndpoint=XXXXXX;AccountKey=XXXXXX;."); + throw new InvalidOperationException($"The attribute {nameof(attribute.Connection)} for the monitored container is in an invalid format, please use AccountEndpoint=XXXXXX;AccountKey=XXXXXX; or a node representing token authentication information."); } - string leasesConnectionString = ResolveAttributeLeasesConnectionString(attribute); - if (string.IsNullOrEmpty(leasesConnectionString)) + string leasesConnection = ResolveAttributeLeasesConnection(attribute); + if (string.IsNullOrEmpty(leasesConnection)) { - throw new InvalidOperationException("The connection string for the leases container is in an invalid format, please use AccountEndpoint=XXXXXX;AccountKey=XXXXXX;."); + throw new InvalidOperationException($"The attribute {nameof(attribute.LeaseConnection)} for the leases container is in an invalid format, please use AccountEndpoint=XXXXXX;AccountKey=XXXXXX;. or a node representing token authentication information."); } if (string.IsNullOrEmpty(monitoredDatabaseName) @@ -81,7 +78,7 @@ public async Task TryCreateAsync(TriggerBindingProviderContext throw new InvalidOperationException("Cannot establish database and container values. If you are using environment and configuration values, please ensure these are correctly set."); } - if (triggerConnectionString.Equals(leasesConnectionString, StringComparison.InvariantCultureIgnoreCase) + if (triggerConnection.Equals(leasesConnection, StringComparison.InvariantCultureIgnoreCase) && monitoredDatabaseName.Equals(leasesDatabaseName, StringComparison.InvariantCultureIgnoreCase) && monitoredCollectionName.Equals(leasesCollectionName, StringComparison.InvariantCultureIgnoreCase)) { @@ -89,11 +86,11 @@ public async Task TryCreateAsync(TriggerBindingProviderContext } CosmosClient monitoredCosmosDBService = _configProvider.GetService( - connectionString: triggerConnectionString, + connection: triggerConnection, preferredLocations: preferredLocations, userAgent: CosmosDBTriggerUserAgentSuffix); CosmosClient leaseCosmosDBService = _configProvider.GetService( - connectionString: leasesConnectionString, + connection: leasesConnection, preferredLocations: preferredLocations, userAgent: CosmosDBTriggerUserAgentSuffix); @@ -148,9 +145,9 @@ private static async Task CreateLeaseCollectionIfNotExistsAsync(CosmosClient cos } } - private string ResolveAttributeConnectionString(CosmosDBTriggerAttribute attribute) + private string ResolveAttributeConnection(CosmosDBTriggerAttribute attribute) { - string connectionString = ResolveConnectionString(attribute.Connection, nameof(CosmosDBTriggerAttribute.Connection)); + string connectionString = attribute.Connection ?? Constants.DefaultConnectionStringName; if (string.IsNullOrEmpty(connectionString)) { @@ -160,7 +157,7 @@ private string ResolveAttributeConnectionString(CosmosDBTriggerAttribute attribu return connectionString; } - private string ResolveAttributeLeasesConnectionString(CosmosDBTriggerAttribute attribute) + private string ResolveAttributeLeasesConnection(CosmosDBTriggerAttribute attribute) { // If the lease connection string is not set, use the trigger's string keyToResolve = attribute.LeaseConnection; @@ -169,7 +166,7 @@ private string ResolveAttributeLeasesConnectionString(CosmosDBTriggerAttribute a keyToResolve = attribute.Connection; } - string connectionString = ResolveConnectionString(keyToResolve, nameof(CosmosDBTriggerAttribute.LeaseConnection)); + string connectionString = keyToResolve ?? Constants.DefaultConnectionStringName; if (string.IsNullOrEmpty(connectionString)) { @@ -185,31 +182,10 @@ private void ThrowMissingConnectionStringException(bool isLeaseConnectionString $"{nameof(CosmosDBTriggerAttribute)}.{nameof(CosmosDBTriggerAttribute.LeaseConnection)}" : $"{nameof(CosmosDBTriggerAttribute)}.{nameof(CosmosDBTriggerAttribute.Connection)}"; - string optionsProperty = $"{nameof(CosmosDBOptions)}.{nameof(CosmosDBOptions.ConnectionString)}"; - string leaseString = isLeaseConnectionString ? "lease " : string.Empty; throw new InvalidOperationException( - $"The CosmosDBTrigger {leaseString}connection string must be set either via a '{Constants.DefaultConnectionStringName}' configuration connection string, via the {attributeProperty} property or via {optionsProperty}."); - } - - internal string ResolveConnectionString(string unresolvedConnectionString, string propertyName) - { - // First, resolve the string. - if (!string.IsNullOrEmpty(unresolvedConnectionString)) - { - string resolvedString = _configuration.GetConnectionStringOrSetting(unresolvedConnectionString); - - if (string.IsNullOrEmpty(resolvedString)) - { - throw new InvalidOperationException($"Unable to resolve app setting for property '{nameof(CosmosDBTriggerAttribute)}.{propertyName}'. Make sure the app setting exists and has a valid value."); - } - - return resolvedString; - } - - // If that didn't exist, fall back to options. - return _options.ConnectionString; + $"The CosmosDBTrigger {leaseString}connection must be set either via a '{Constants.DefaultConnectionStringName}' configuration or via the {attributeProperty} property."); } private string ResolveAttributeValue(string attributeValue) diff --git a/src/WebJobs.Extensions.CosmosDB/Trigger/CosmosDBTriggerAttributeBindingProviderGenerator.cs b/src/WebJobs.Extensions.CosmosDB/Trigger/CosmosDBTriggerAttributeBindingProviderGenerator.cs index ba88f29c3..6697c9388 100644 --- a/src/WebJobs.Extensions.CosmosDB/Trigger/CosmosDBTriggerAttributeBindingProviderGenerator.cs +++ b/src/WebJobs.Extensions.CosmosDB/Trigger/CosmosDBTriggerAttributeBindingProviderGenerator.cs @@ -16,16 +16,14 @@ namespace Microsoft.Azure.WebJobs.Extensions.CosmosDB /// internal class CosmosDBTriggerAttributeBindingProviderGenerator : ITriggerBindingProvider { - private readonly IConfiguration _configuration; private readonly INameResolver _nameResolver; private readonly CosmosDBOptions _options; private readonly ILoggerFactory _loggerFactory; private readonly CosmosDBExtensionConfigProvider _configProvider; - public CosmosDBTriggerAttributeBindingProviderGenerator(IConfiguration configuration, INameResolver nameResolver, CosmosDBOptions options, + public CosmosDBTriggerAttributeBindingProviderGenerator(INameResolver nameResolver, CosmosDBOptions options, CosmosDBExtensionConfigProvider configProvider, ILoggerFactory loggerFactory) { - _configuration = configuration; _nameResolver = nameResolver; _options = options; _configProvider = configProvider; @@ -59,11 +57,11 @@ public Task TryCreateAsync(TriggerBindingProviderContext contex Type genericBindingType = baseType.MakeGenericType(documentType); - Type[] typeArgs = { typeof(IConfiguration), typeof(INameResolver), typeof(CosmosDBOptions), typeof(CosmosDBExtensionConfigProvider), typeof(ILoggerFactory) }; + Type[] typeArgs = { typeof(INameResolver), typeof(CosmosDBOptions), typeof(CosmosDBExtensionConfigProvider), typeof(ILoggerFactory) }; ConstructorInfo constructor = genericBindingType.GetConstructor(typeArgs); - object[] constructorParameterValues = { _configuration, _nameResolver, _options, _configProvider, _loggerFactory }; + object[] constructorParameterValues = { _nameResolver, _options, _configProvider, _loggerFactory }; object cosmosDBTriggerAttributeBindingProvider = constructor.Invoke(constructorParameterValues); diff --git a/src/WebJobs.Extensions.CosmosDB/WebJobs.Extensions.CosmosDB.csproj b/src/WebJobs.Extensions.CosmosDB/WebJobs.Extensions.CosmosDB.csproj index b27f7b29f..6e937e835 100644 --- a/src/WebJobs.Extensions.CosmosDB/WebJobs.Extensions.CosmosDB.csproj +++ b/src/WebJobs.Extensions.CosmosDB/WebJobs.Extensions.CosmosDB.csproj @@ -8,7 +8,7 @@ - $(CosmosDBVersion) + $(CosmosDBVersion)-preview1 true @@ -22,6 +22,7 @@ + diff --git a/src/WebJobs.Extensions.CosmosDB/WebJobsConfigurationExtensions.cs b/src/WebJobs.Extensions.CosmosDB/WebJobsConfigurationExtensions.cs new file mode 100644 index 000000000..1ef8977e1 --- /dev/null +++ b/src/WebJobs.Extensions.CosmosDB/WebJobsConfigurationExtensions.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Azure.WebJobs.Extensions.CosmosDB +{ + internal static class WebJobsConfigurationExtensions + { + private const string WebJobsConfigurationSectionName = "AzureWebJobs"; + + public static IConfigurationSection GetWebJobsConnectionStringSection(this IConfiguration configuration, string connectionStringName) + { + // first try prefixing + string prefixedConnectionStringName = GetPrefixedConnectionStringName(connectionStringName); + IConfigurationSection section = GetConnectionStringOrSetting(configuration, prefixedConnectionStringName); + + if (!section.Exists()) + { + // next try a direct unprefixed lookup + section = GetConnectionStringOrSetting(configuration, connectionStringName); + } + + return section; + } + + public static string GetPrefixedConnectionStringName(string connectionStringName) + { + return WebJobsConfigurationSectionName + connectionStringName; + } + + /// + /// Looks for a connection string by first checking the ConfigurationStrings section, and then the root. + /// + /// The configuration. + /// The connection string key. + /// + public static IConfigurationSection GetConnectionStringOrSetting(this IConfiguration configuration, string connectionName) + { + if (configuration.GetSection("ConnectionStrings").Exists()) + { + return configuration.GetSection("ConnectionStrings").GetSection(connectionName); + } + + return configuration.GetSection(connectionName); + } + } +} \ No newline at end of file diff --git a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBConfigurationTests.cs b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBConfigurationTests.cs index 5155d0d3e..3010efc3f 100644 --- a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBConfigurationTests.cs +++ b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBConfigurationTests.cs @@ -6,9 +6,11 @@ using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Extensions.Tests.Common; using Microsoft.Azure.WebJobs.Extensions.Tests.Extensions.CosmosDB.Models; +using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Moq; using Newtonsoft.Json.Linq; using Xunit; @@ -16,14 +18,18 @@ namespace Microsoft.Azure.WebJobs.Extensions.CosmosDB.Tests { public class CosmosDBConfigurationTests { - private static readonly IConfiguration _emptyConfig = new ConfigurationBuilder().Build(); + private static readonly IConfiguration _baseConfig = CosmosDBTestUtility.BuildConfiguration(new List>() + { + Tuple.Create(Constants.DefaultConnectionStringName, "AccountEndpoint=https://defaultUri;AccountKey=c29tZV9rZXk=;"), + Tuple.Create("Attribute", "AccountEndpoint=https://attributeUri;AccountKey=c29tZV9rZXk=;") + }); [Fact] public async Task Configuration_Caches_Clients() { // Arrange - var options = new CosmosDBOptions { ConnectionString = "AccountEndpoint=https://someuri;AccountKey=c29tZV9rZXk=;" }; - var config = new CosmosDBExtensionConfigProvider(new OptionsWrapper(options), new DefaultCosmosDBServiceFactory(), new DefaultCosmosDBSerializerFactory(), _emptyConfig, new TestNameResolver(), NullLoggerFactory.Instance); + var options = new CosmosDBOptions(); + var config = new CosmosDBExtensionConfigProvider(new OptionsWrapper(options), new DefaultCosmosDBServiceFactory(_baseConfig, Mock.Of()), new DefaultCosmosDBSerializerFactory(), new TestNameResolver(), NullLoggerFactory.Instance); var attribute = new CosmosDBAttribute { Id = "abcdef" }; // Act @@ -38,37 +44,25 @@ public async Task Configuration_Caches_Clients() [Fact] public void Resolve_UsesAttribute_First() { - var config = InitializeExtensionConfigProvider("Default", "Config"); - - // Act - var connString = config.ResolveConnectionString("Attribute"); - - // Assert - Assert.Equal("Attribute", connString); - } - - [Fact] - public void Resolve_UsesConfig_Second() - { - var config = InitializeExtensionConfigProvider("Default", "Config"); + var config = InitializeExtensionConfigProvider(); // Act - var connString = config.ResolveConnectionString(null); + var context = config.CreateContext(new CosmosDBAttribute() { Connection = "Attribute" }); // Assert - Assert.Equal("Config", connString); + Assert.True(context.Service.Endpoint.ToString().Contains("attribute")); } [Fact] public void Resolve_UsesDefault_Last() { - var config = InitializeExtensionConfigProvider("Default"); + var config = InitializeExtensionConfigProvider(); // Act - var connString = config.ResolveConnectionString(null); + var context = config.CreateContext(new CosmosDBAttribute()); // Assert - Assert.Equal("Default", connString); + Assert.True(context.Service.Endpoint.ToString().Contains("default")); } [Theory] @@ -84,12 +78,12 @@ public void TryGetEnumerableType(Type type, bool expectedResult) Assert.Equal(expectedResult, actualResult); } - private CosmosDBExtensionConfigProvider InitializeExtensionConfigProvider(string defaultConnStr, string optionsConnStr = null) + private CosmosDBExtensionConfigProvider InitializeExtensionConfigProvider() { - var options = CosmosDBTestUtility.InitializeOptions(defaultConnStr, optionsConnStr); - var factory = new DefaultCosmosDBServiceFactory(); + var options = new CosmosDBOptions(); + var factory = new DefaultCosmosDBServiceFactory(_baseConfig, Mock.Of()); var nameResolver = new TestNameResolver(); - var configProvider = new CosmosDBExtensionConfigProvider(options, factory, new DefaultCosmosDBSerializerFactory(), _emptyConfig, nameResolver, NullLoggerFactory.Instance); + var configProvider = new CosmosDBExtensionConfigProvider(new OptionsWrapper(options), factory, new DefaultCosmosDBSerializerFactory(), nameResolver, NullLoggerFactory.Instance); var context = TestHelpers.CreateExtensionConfigContext(nameResolver); diff --git a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBEndToEndTests.cs b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBEndToEndTests.cs index f74c4ce39..d68e5dd4d 100644 --- a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBEndToEndTests.cs +++ b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBEndToEndTests.cs @@ -10,10 +10,13 @@ using Microsoft.Azure.WebJobs.Extensions.Tests; using Microsoft.Azure.WebJobs.Extensions.Tests.Common; using Microsoft.Azure.WebJobs.Extensions.Tests.Extensions.CosmosDB.Models; +using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Moq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; @@ -75,7 +78,7 @@ await TestHelpers.Await(() => private async Task InitializeDocumentClientAsync(IConfiguration configuration) { - var client = new CosmosClient(configuration.GetConnectionStringOrSetting(Constants.DefaultConnectionStringName)); + var client = new CosmosClient(configuration.GetConnectionStringOrSetting(Constants.DefaultConnectionStringName).Value); Database database = await client.CreateDatabaseIfNotExistsAsync(DatabaseName); @@ -109,6 +112,7 @@ private async Task StartHostAsync(Type testType) .ConfigureServices(services => { services.AddSingleton(locator); + services.AddAzureClientsCore(); }) .ConfigureLogging(logging => { diff --git a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBEnumerableBuilderTests.cs b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBEnumerableBuilderTests.cs index 7ea6750ac..b60462de9 100644 --- a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBEnumerableBuilderTests.cs +++ b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBEnumerableBuilderTests.cs @@ -22,7 +22,10 @@ public class CosmosDBEnumerableBuilderTests { private const string DatabaseName = "ItemDb"; private const string CollectionName = "ItemCollection"; - private static readonly IConfiguration _emptyConfig = new ConfigurationBuilder().Build(); + private static readonly IConfiguration _baseConfig = CosmosDBTestUtility.BuildConfiguration(new List>() + { + Tuple.Create(Constants.DefaultConnectionStringName, "AccountEndpoint=https://defaultUri;AccountKey=c29tZV9rZXk=;") + }); [Fact] public async Task ConvertAsync_Succeeds_NoContinuation() @@ -217,9 +220,8 @@ private static CosmosDBEnumerableBuilder CreateBuilder(out Mock(new CosmosDBOptions { - ConnectionString = "AccountEndpoint=https://someuri;AccountKey=c29tZV9rZXk=;" }); - var configProvider = new CosmosDBExtensionConfigProvider(options, mockServiceFactory.Object, new DefaultCosmosDBSerializerFactory(), _emptyConfig, new TestNameResolver(), NullLoggerFactory.Instance); + var configProvider = new CosmosDBExtensionConfigProvider(options, mockServiceFactory.Object, new DefaultCosmosDBSerializerFactory(), new TestNameResolver(), NullLoggerFactory.Instance); return new CosmosDBEnumerableBuilder(configProvider); } diff --git a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBHostBuilderExtensionsTests.cs b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBHostBuilderExtensionsTests.cs index 0086d3f46..79fdb5614 100644 --- a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBHostBuilderExtensionsTests.cs +++ b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBHostBuilderExtensionsTests.cs @@ -7,10 +7,14 @@ using System.Linq; using Microsoft.Azure.Cosmos; using Microsoft.Azure.WebJobs.Host.Config; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using Moq; using Xunit; namespace Microsoft.Azure.WebJobs.Extensions.CosmosDB.Tests @@ -71,6 +75,14 @@ public void ConfigurationBindsToOptions_WithSerializer() { CustomFactory customFactory = new CustomFactory(); IHost host = new HostBuilder() + .ConfigureAppConfiguration(c => + { + c.Sources.Clear(); + c.AddInMemoryCollection(new Dictionary + { + { Constants.DefaultConnectionStringName, "AccountEndpoint=https://defaultUri;AccountKey=c29tZV9rZXk=;" } + }); + }) .ConfigureWebJobs(builder => { builder.AddCosmosDB(); @@ -78,6 +90,7 @@ public void ConfigurationBindsToOptions_WithSerializer() .ConfigureServices(s => { s.AddSingleton(customFactory); + s.TryAddSingleton(Mock.Of()); }) .Build(); @@ -86,7 +99,7 @@ public void ConfigurationBindsToOptions_WithSerializer() Assert.IsType(extensionConfig); CosmosDBExtensionConfigProvider cosmosDBExtensionConfigProvider = (CosmosDBExtensionConfigProvider)extensionConfig; - CosmosClient dummyClient = cosmosDBExtensionConfigProvider.GetService("AccountEndpoint=https://someuri;AccountKey=c29tZV9rZXk=;"); + CosmosClient dummyClient = cosmosDBExtensionConfigProvider.GetService(Constants.DefaultConnectionStringName); Assert.True(customFactory.CreateWasCalled); } diff --git a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBMockEndToEndTests.cs b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBMockEndToEndTests.cs index 3a6995ff4..3001a55a9 100644 --- a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBMockEndToEndTests.cs +++ b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBMockEndToEndTests.cs @@ -11,6 +11,7 @@ using Microsoft.Azure.WebJobs.Extensions.Tests.Extensions.CosmosDB.Models; using Microsoft.Azure.WebJobs.Host; using Microsoft.Azure.WebJobs.Host.Indexers; +using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -27,9 +28,14 @@ public class CosmosDBMockEndToEndTests private const string CollectionName = "TestCollection"; private const string AttributeConnStr = "AccountEndpoint=https://attribute;AccountKey=YXR0cmlidXRl;"; - private const string ConfigConnStr = "AccountEndpoint=https://config;AccountKey=Y29uZmln;"; private const string DefaultConnStr = "AccountEndpoint=https://default;AccountKey=ZGVmYXVsdA==;"; + private static readonly IConfiguration _baseConfig = CosmosDBTestUtility.BuildConfiguration(new List>() + { + Tuple.Create(Constants.DefaultConnectionStringName, DefaultConnStr), + Tuple.Create("MyConnectionString", AttributeConnStr) + }); + private readonly ILoggerFactory _loggerFactory = new LoggerFactory(); private readonly TestLoggerProvider _loggerProvider = new TestLoggerProvider(); @@ -76,14 +82,14 @@ public async Task OutputBindings() var factoryMock = new Mock(MockBehavior.Strict); factoryMock - .Setup(f => f.CreateService(ConfigConnStr, It.IsAny())) + .Setup(f => f.CreateService(Constants.DefaultConnectionStringName, It.IsAny())) .Returns(serviceMock.Object); //Act await RunTestAsync("Outputs", factoryMock.Object); // Assert - factoryMock.Verify(f => f.CreateService(ConfigConnStr, It.IsAny()), Times.Once()); + factoryMock.Verify(f => f.CreateService(Constants.DefaultConnectionStringName, It.IsAny()), Times.Once()); mockContainer.Verify(m => m.UpsertItemAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(7)); mockContainer.Verify(m => m.UpsertItemAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); Assert.Equal("Outputs", _loggerProvider.GetAllUserLogMessages().Single().FormattedMessage); @@ -95,15 +101,15 @@ public async Task ClientBinding() // Arrange var factoryMock = new Mock(MockBehavior.Strict); factoryMock - .Setup(f => f.CreateService(DefaultConnStr, It.IsAny())) - .Returns((connectionString, connectionPolicy) => new CosmosClient(connectionString, connectionPolicy)); + .Setup(f => f.CreateService(Constants.DefaultConnectionStringName, It.IsAny())) + .Returns((connectionString, connectionPolicy) => new CosmosClient(DefaultConnStr, connectionPolicy)); // Act // Also verify that this falls back to the default by setting the config connection string to null - await RunTestAsync("Client", factoryMock.Object, configConnectionString: null); + await RunTestAsync("Client", factoryMock.Object); //Assert - factoryMock.Verify(f => f.CreateService(DefaultConnStr, It.IsAny()), Times.Once()); + factoryMock.Verify(f => f.CreateService(Constants.DefaultConnectionStringName, It.IsAny()), Times.Once()); Assert.Equal("Client", _loggerProvider.GetAllUserLogMessages().Single().FormattedMessage); } @@ -321,8 +327,8 @@ public async Task TriggerObject() public async Task NoConnectionStringSet() { // Act - var ex = await Assert.ThrowsAsync( - () => RunTestAsync(typeof(NoConnectionString), nameof(NoConnectionString.Broken), new DefaultCosmosDBServiceFactory(), configConnectionString: null, includeDefaultConnectionString: false)); + var ex = await Assert.ThrowsAsync( + () => RunTestAsync(typeof(NoConnectionString), nameof(NoConnectionString.Broken), new DefaultCosmosDBServiceFactory(Mock.Of(), Mock.Of()), includeDefaultConnectionString: false)); // Assert Assert.IsType(ex.InnerException); @@ -333,7 +339,7 @@ public async Task InvalidEnumerableBindings() { // Act var ex = await Assert.ThrowsAsync( - () => RunTestAsync(typeof(InvalidEnumerable), nameof(InvalidEnumerable.BrokenEnumerable), new DefaultCosmosDBServiceFactory())); + () => RunTestAsync(typeof(InvalidEnumerable), nameof(InvalidEnumerable.BrokenEnumerable), new DefaultCosmosDBServiceFactory(_baseConfig, Mock.Of()))); // Assert Assert.IsType(ex.InnerException); @@ -347,7 +353,7 @@ public async Task InvalidItemBindings() { // Act var ex = await Assert.ThrowsAsync( - () => RunTestAsync(typeof(InvalidItem), nameof(InvalidItem.BrokenItem), new DefaultCosmosDBServiceFactory())); + () => RunTestAsync(typeof(InvalidItem), nameof(InvalidItem.BrokenItem), new DefaultCosmosDBServiceFactory(_baseConfig, Mock.Of()))); // Assert Assert.IsType(ex.InnerException); @@ -362,7 +368,7 @@ public async Task NoByteArrayEnumerableBindings() // byte[] isn't supported by DocumentClient, so we need to make sure we reject it early. // Act var ex = await Assert.ThrowsAsync( - () => RunTestAsync(typeof(InvalidByteArrayEnumerable), nameof(InvalidByteArrayEnumerable.BrokenEnumerable), new DefaultCosmosDBServiceFactory())); + () => RunTestAsync(typeof(InvalidByteArrayEnumerable), nameof(InvalidByteArrayEnumerable.BrokenEnumerable), new DefaultCosmosDBServiceFactory(_baseConfig, Mock.Of()))); // Assert Assert.IsType(ex.InnerException); @@ -375,7 +381,7 @@ public async Task NoByteArrayItemBindings() // byte[] isn't supported by DocumentClient, so we need to make sure we reject it early. // Act var ex = await Assert.ThrowsAsync( - () => RunTestAsync(typeof(InvalidByteArrayItem), nameof(InvalidByteArrayItem.BrokenItem), new DefaultCosmosDBServiceFactory())); + () => RunTestAsync(typeof(InvalidByteArrayItem), nameof(InvalidByteArrayItem.BrokenItem), new DefaultCosmosDBServiceFactory(_baseConfig, Mock.Of()))); // Assert Assert.IsType(ex.InnerException); @@ -388,19 +394,19 @@ public async Task NoByteArrayCollectorBindings() // byte[] isn't supported by DocumentClient, so we need to make sure we reject it early. // Act var ex = await Assert.ThrowsAsync( - () => RunTestAsync(typeof(InvalidByteArrayCollector), nameof(InvalidByteArrayCollector.BrokenCollector), new DefaultCosmosDBServiceFactory())); + () => RunTestAsync(typeof(InvalidByteArrayCollector), nameof(InvalidByteArrayCollector.BrokenCollector), new DefaultCosmosDBServiceFactory(_baseConfig, Mock.Of()))); // Assert Assert.IsType(ex.InnerException); Assert.StartsWith("Nested collections are not supported", ex.InnerException.Message); } - private Task RunTestAsync(string testName, ICosmosDBServiceFactory factory, object argument = null, string configConnectionString = ConfigConnStr) + private Task RunTestAsync(string testName, ICosmosDBServiceFactory factory, object argument = null) { - return RunTestAsync(typeof(CosmosDBEndToEndFunctions), testName, factory, argument, configConnectionString); + return RunTestAsync(typeof(CosmosDBEndToEndFunctions), testName, factory, argument); } - private async Task RunTestAsync(Type testType, string testName, ICosmosDBServiceFactory factory, object argument = null, string configConnectionString = ConfigConnStr, bool includeDefaultConnectionString = true) + private async Task RunTestAsync(Type testType, string testName, ICosmosDBServiceFactory factory, object argument = null, bool includeDefaultConnectionString = true) { ExplicitTypeLocator locator = new ExplicitTypeLocator(testType); @@ -438,11 +444,6 @@ private async Task RunTestAsync(Type testType, string testName, ICosmosDBService services.AddSingleton(factory); services.AddSingleton(resolver); services.AddSingleton(locator); - - if (configConnectionString != null) - { - services.Configure(o => o.ConnectionString = configConnectionString); - } }) .ConfigureLogging(logging => { diff --git a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBTestUtility.cs b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBTestUtility.cs index 5eb8c3649..62af71b0a 100644 --- a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBTestUtility.cs +++ b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBTestUtility.cs @@ -12,9 +12,6 @@ using Microsoft.Azure.WebJobs.Extensions.Tests.Extensions.CosmosDB.Models; using Microsoft.Azure.WebJobs.Host.Bindings; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; using Moq; using Newtonsoft.Json.Linq; @@ -25,40 +22,6 @@ internal static class CosmosDBTestUtility public const string DatabaseName = "ItemDB"; public const string ContainerName = "ItemCollection"; - // Runs the standard options pipeline for initialization - public static IOptions InitializeOptions(string defaultConnectionString, string optionsConnectionString) - { - // Create the Options like we do during host build - var builder = new HostBuilder() - .ConfigureWebJobs(b => - { - // This wires up our options pipeline. - b.AddCosmosDB(); - - // If someone is updating the ConnectionString, they'd do it like this. - if (optionsConnectionString != null) - { - b.Services.Configure(o => - { - o.ConnectionString = optionsConnectionString; - }); - } - }) - .ConfigureAppConfiguration(b => - { - b.Sources.Clear(); - if (defaultConnectionString != null) - { - b.AddInMemoryCollection(new Dictionary - { - { $"ConnectionStrings:{Constants.DefaultConnectionStringName}", defaultConnectionString } - }); - } - }); - - return builder.Build().Services.GetService>(); - } - public static Mock SetupCollectionMock(Mock mockService, Mock mockDatabase, string partitionKeyPath = null, int throughput = 0) { var mockContainer = new Mock(MockBehavior.Strict); @@ -140,6 +103,19 @@ public static CosmosDBContext CreateContext(CosmosClient service, bool createIfN }; } + public static IConfiguration BuildConfiguration(List> configs) + { + var mock = new Mock(); + foreach (var config in configs) + { + var section = new Mock(); + section.Setup(s => s.Value).Returns(config.Item2); + mock.Setup(c => c.GetSection(It.Is(sectionName => sectionName == config.Item1))).Returns(section.Object); + } + + return mock.Object; + } + public static IOrderedQueryable AsOrderedQueryable(this IEnumerable enumerable, Expression> keySelector) { return enumerable.AsQueryable().OrderBy(keySelector); diff --git a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBWebJobsStartupTests.cs b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBWebJobsStartupTests.cs index e21704e59..a2dd0516d 100644 --- a/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBWebJobsStartupTests.cs +++ b/test/WebJobs.Extensions.CosmosDB.Tests/CosmosDBWebJobsStartupTests.cs @@ -6,8 +6,11 @@ using System.Reflection; using Microsoft.Azure.WebJobs.Host.Config; using Microsoft.Azure.WebJobs.Hosting; +using Microsoft.Extensions.Azure; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; +using Moq; using Xunit; namespace Microsoft.Azure.WebJobs.Extensions.CosmosDB.Tests @@ -23,6 +26,10 @@ public void CosmosDBStartupIsDiscoverable() { builder.UseExternalStartup(new TestStartupTypeLocator()); }) + .ConfigureServices(s => + { + s.TryAddSingleton(Mock.Of()); + }) .Build(); var extensionConfig = host.Services.GetServices().Single(); diff --git a/test/WebJobs.Extensions.CosmosDB.Tests/DefaultCosmosDBServiceFactoryTests.cs b/test/WebJobs.Extensions.CosmosDB.Tests/DefaultCosmosDBServiceFactoryTests.cs new file mode 100644 index 000000000..e2c19e168 --- /dev/null +++ b/test/WebJobs.Extensions.CosmosDB.Tests/DefaultCosmosDBServiceFactoryTests.cs @@ -0,0 +1,127 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Azure.Core; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Moq; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Extensions.CosmosDB.Tests +{ + public class DefaultCosmosDBServiceFactoryTests + { + [Fact] + public void UsesDefaultConnection() + { + // Arrange + var config = CosmosDBTestUtility.BuildConfiguration(new List>() + { + Tuple.Create(Constants.DefaultConnectionStringName, "AccountEndpoint=https://defaultUri;AccountKey=c29tZV9rZXk=;"), + }); + + var factory = new DefaultCosmosDBServiceFactory(config, Mock.Of()); + var options = new CosmosClientOptions() + { + ApplicationName = Guid.NewGuid().ToString() + }; + + // Act + var client = factory.CreateService(null, options); + + // Assert + Assert.NotNull(client); + Assert.True(client.Endpoint.ToString().Contains("default")); + Assert.Equal(options.ApplicationName, client.ClientOptions.ApplicationName); + } + + [Fact] + public void UsesConfig() + { + // Arrange + var config = CosmosDBTestUtility.BuildConfiguration(new List>() + { + Tuple.Create(Constants.DefaultConnectionStringName, "AccountEndpoint=https://defaultUri;AccountKey=c29tZV9rZXk=;"), + Tuple.Create("Attribute", "AccountEndpoint=https://attributeUri;AccountKey=c29tZV9rZXk=;") + }); + + var factory = new DefaultCosmosDBServiceFactory(config, Mock.Of()); + + // Act + var client = factory.CreateService("Attribute", new CosmosClientOptions()); + + // Assert + Assert.NotNull(client); + Assert.True(client.Endpoint.ToString().Contains("attribute")); + } + + [Fact] + public void FailsIfNotExists() + { + // Arrange + var config = CosmosDBTestUtility.BuildConfiguration(new List>() + { + Tuple.Create(Constants.DefaultConnectionStringName, "AccountEndpoint=https://defaultUri;AccountKey=c29tZV9rZXk=;") + }); + + var factory = new DefaultCosmosDBServiceFactory(config, Mock.Of()); + + // Assert + Assert.Throws(() => factory.CreateService("Attribute", new CosmosClientOptions())); + } + + [Fact] + public void CreatesCredentials_NoEndpoint() + { + // Arrange + var myConfiguration = new Dictionary + { + { "Credentials:tenantId", "ConfigurationTenant" }, + { "Credentials:clientId", "ConfigurationClientId" }, + { "Credentials:clientSecret", "ConfigurationSecret" } + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(myConfiguration) + .Build(); + + var factory = new DefaultCosmosDBServiceFactory(configuration, Mock.Of()); + + // Act + Assert.Throws(() => factory.CreateService("Credentials", new CosmosClientOptions())); + } + + [Fact] + public void CreatesCredentials() + { + // Arrange + var myConfiguration = new Dictionary + { + { "Credentials:accountEndpoint", "http://someEndpoint" }, + { "Credentials:tenantId", "ConfigurationTenant" }, + { "Credentials:clientId", "ConfigurationClientId" }, + { "Credentials:clientSecret", "ConfigurationSecret" } + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(myConfiguration) + .Build(); + + var componentFactoryMock = new Mock(); + componentFactoryMock.Setup(f => f.CreateTokenCredential(It.Is(section => section.Key == "Credentials"))) + .Returns(Mock.Of()); + + var factory = new DefaultCosmosDBServiceFactory(configuration, componentFactoryMock.Object); + + // Act + var client = factory.CreateService("Credentials", new CosmosClientOptions()); + + // Assert + Assert.NotNull(client); + Assert.True(client.Endpoint.ToString().Contains("someendpoint")); + } + } +} diff --git a/test/WebJobs.Extensions.CosmosDB.Tests/Trigger/CosmosDBTriggerAttributeBindingProviderTests.cs b/test/WebJobs.Extensions.CosmosDB.Tests/Trigger/CosmosDBTriggerAttributeBindingProviderTests.cs index d566022ca..760fb9fa5 100644 --- a/test/WebJobs.Extensions.CosmosDB.Tests/Trigger/CosmosDBTriggerAttributeBindingProviderTests.cs +++ b/test/WebJobs.Extensions.CosmosDB.Tests/Trigger/CosmosDBTriggerAttributeBindingProviderTests.cs @@ -12,6 +12,7 @@ using Microsoft.Azure.WebJobs.Extensions.CosmosDB.Tests; using Microsoft.Azure.WebJobs.Extensions.Tests.Common; using Microsoft.Azure.WebJobs.Host.Triggers; +using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -25,8 +26,12 @@ namespace Microsoft.Azure.WebJobs.Extensions.CosmosDBTrigger.Tests public class CosmosDBTriggerAttributeBindingProviderTests { private readonly ILoggerFactory _loggerFactory = new LoggerFactory(); - private static readonly IConfiguration _emptyConfig = new ConfigurationBuilder().Build(); - private readonly CosmosDBOptions _options = CosmosDBTestUtility.InitializeOptions("AccountEndpoint=https://fromEnvironment;AccountKey=c29tZV9rZXk=;", null).Value; + private static readonly IConfiguration _baseConfig = CosmosDBTestUtility.BuildConfiguration(new List>() + { + Tuple.Create(Constants.DefaultConnectionStringName, "AccountEndpoint=https://fromEnvironment;AccountKey=c29tZV9rZXk=;") + }); + + private readonly CosmosDBOptions _options = new CosmosDBOptions(); public static IEnumerable ValidCosmosDBTriggerBindigsWithLeaseHostOptionsParameters => ValidCosmosDBTriggerBindigsWithLeaseHostOptions.GetParameters(); @@ -73,7 +78,7 @@ public static IEnumerable ValidCosmosDBTriggerBindingsCreateLeaseConta [MemberData(nameof(InvalidCosmosDBTriggerParameters))] public async Task InvalidParameters_Fail(ParameterInfo parameter) { - CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(_emptyConfig, new TestNameResolver(), _options, CreateExtensionConfigProvider(_options), _loggerFactory); + CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(new TestNameResolver(), _options, CreateExtensionConfigProvider(_options, _baseConfig), _loggerFactory); InvalidOperationException ex = await Assert.ThrowsAsync(() => provider.TryCreateAsync(new TriggerBindingProviderContext(parameter, CancellationToken.None))); @@ -88,11 +93,12 @@ public async Task ValidParametersWithEnvironment_Succeed(ParameterInfo parameter var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { + { "ConnectionStrings:CosmosDB", "AccountEndpoint=https://fromEnvironment;AccountKey=c29tZV9rZXk=;" }, { "ConnectionStrings:CosmosDBConnectionString", "AccountEndpoint=https://fromSettings;AccountKey=c29tZV9rZXk=;" } }) .Build(); - CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(config, nameResolver, _options, CreateExtensionConfigProvider(_options), _loggerFactory); + CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(nameResolver, _options, CreateExtensionConfigProvider(_options, config), _loggerFactory); CosmosDBTriggerBinding binding = (CosmosDBTriggerBinding)await provider.TryCreateAsync(new TriggerBindingProviderContext(parameter, CancellationToken.None)); @@ -118,7 +124,7 @@ public async Task ValidParametersWithAppSettings_Succeed(ParameterInfo parameter }) .Build(); - CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(config, nameResolver, _options, CreateExtensionConfigProvider(_options), _loggerFactory); + CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(nameResolver, _options, CreateExtensionConfigProvider(_options, config), _loggerFactory); CosmosDBTriggerBinding binding = (CosmosDBTriggerBinding)await provider.TryCreateAsync(new TriggerBindingProviderContext(parameter, CancellationToken.None)); @@ -140,7 +146,7 @@ public async Task ValidCosmosDBTriggerBindigsWithDatabaseAndCollectionSettings_S nameResolver.Values["aDatabase"] = "myDatabase"; nameResolver.Values["aCollection"] = "myCollection"; - CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(_emptyConfig, nameResolver, _options, CreateExtensionConfigProvider(_options), _loggerFactory); + CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(nameResolver, _options, CreateExtensionConfigProvider(_options, _baseConfig), _loggerFactory); CosmosDBTriggerBinding binding = (CosmosDBTriggerBinding)await provider.TryCreateAsync(new TriggerBindingProviderContext(parameter, CancellationToken.None)); @@ -169,7 +175,7 @@ public async Task ValidCosmosDBTriggerBindigsDifferentConnections_Succeed(Parame }) .Build(); - CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(config, nameResolver, _options, CreateExtensionConfigProvider(_options), _loggerFactory); + CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(nameResolver, _options, CreateExtensionConfigProvider(_options, config), _loggerFactory); CosmosDBTriggerBinding binding = (CosmosDBTriggerBinding)await provider.TryCreateAsync(new TriggerBindingProviderContext(parameter, CancellationToken.None)); @@ -186,13 +192,14 @@ public async Task ValidParametersWithEnvironment_ConnectionMode_Succeed(Paramete var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { + { "ConnectionStrings:CosmosDB", "AccountEndpoint=https://fromEnvironment;AccountKey=c29tZV9rZXk=;" }, { "ConnectionStrings:CosmosDBConnectionString", "AccountEndpoint=https://fromSettings;AccountKey=c29tZV9rZXk=;" } }) .Build(); _options.ConnectionMode = ConnectionMode.Direct; - CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(config, nameResolver, _options, CreateExtensionConfigProvider(_options), _loggerFactory); + CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(nameResolver, _options, CreateExtensionConfigProvider(_options, config), _loggerFactory); CosmosDBTriggerBinding binding = (CosmosDBTriggerBinding)await provider.TryCreateAsync(new TriggerBindingProviderContext(parameter, CancellationToken.None)); @@ -209,7 +216,7 @@ public async Task ValidCosmosDBTriggerBindigsPreferredLocationsParameters_Succee var nameResolver = new TestNameResolver(); nameResolver.Values["regions"] = "East US, North Europe,"; - CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(_emptyConfig, nameResolver, _options, CreateExtensionConfigProvider(_options), _loggerFactory); + CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(nameResolver, _options, CreateExtensionConfigProvider(_options, _baseConfig), _loggerFactory); CosmosDBTriggerBinding binding = (CosmosDBTriggerBinding)await provider.TryCreateAsync(new TriggerBindingProviderContext(parameter, CancellationToken.None)); @@ -250,6 +257,7 @@ public async Task ValidLeaseHostOptions_Succeed(ParameterInfo parameter) .AddInMemoryCollection(new Dictionary { // Verify we load from connection strings first + { "ConnectionStrings:CosmosDB", "AccountEndpoint=https://fromEnvironment;AccountKey=c29tZV9rZXk=;" }, { "ConnectionStrings:CosmosDBConnectionString", "AccountEndpoint=https://fromSettings;AccountKey=c29tZV9rZXk=;" }, { "CosmosDBConnectionString", "will not work" }, { "ConnectionStrings:LeaseConnectionString", "AccountEndpoint=https://overridden;AccountKey=c29tZV9rZXk=;" }, @@ -257,7 +265,7 @@ public async Task ValidLeaseHostOptions_Succeed(ParameterInfo parameter) }) .Build(); - CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(config, nameResolver, _options, CreateExtensionConfigProvider(_options), _loggerFactory); + CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(nameResolver, _options, CreateExtensionConfigProvider(_options, config), _loggerFactory); CosmosDBTriggerBinding binding = (CosmosDBTriggerBinding)await provider.TryCreateAsync(new TriggerBindingProviderContext(parameter, CancellationToken.None)); @@ -313,7 +321,7 @@ public async Task ValidCreateIfNotExists(ParameterInfo parameter) .Setup(f => f.CreateService(It.IsAny(), It.IsAny())) .Returns(serviceMock.Object); - CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(config, new TestNameResolver(), _options, CreateExtensionConfigProvider(factoryMock.Object, _options), _loggerFactory); + CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(new TestNameResolver(), _options, CreateExtensionConfigProvider(factoryMock.Object, _options, config), _loggerFactory); CosmosDBTriggerBinding binding = (CosmosDBTriggerBinding)await provider.TryCreateAsync(new TriggerBindingProviderContext(parameter, CancellationToken.None)); @@ -371,7 +379,7 @@ public async Task ValidCreateIfNotExistsForGremlin(ParameterInfo parameter) .Setup(f => f.CreateService(It.IsAny(), It.IsAny())) .Returns(serviceMock.Object); - CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(config, new TestNameResolver(), _options, CreateExtensionConfigProvider(factoryMock.Object, _options), _loggerFactory); + CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(new TestNameResolver(), _options, CreateExtensionConfigProvider(factoryMock.Object, _options, config), _loggerFactory); CosmosDBTriggerBinding binding = (CosmosDBTriggerBinding)await provider.TryCreateAsync(new TriggerBindingProviderContext(parameter, CancellationToken.None)); @@ -396,7 +404,7 @@ public async Task ValidChangeFeedOptions_Succeed(ParameterInfo parameter) }) .Build(); - CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(config, new TestNameResolver(), _options, CreateExtensionConfigProvider(_options), _loggerFactory); + CosmosDBTriggerAttributeBindingProvider provider = new CosmosDBTriggerAttributeBindingProvider(new TestNameResolver(), _options, CreateExtensionConfigProvider(_options, config), _loggerFactory); CosmosDBTriggerBinding binding = (CosmosDBTriggerBinding)await provider.TryCreateAsync(new TriggerBindingProviderContext(parameter, CancellationToken.None)); @@ -415,14 +423,14 @@ private static ParameterInfo GetFirstParameter(Type type, string methodName) return paramInfo; } - private static CosmosDBExtensionConfigProvider CreateExtensionConfigProvider(CosmosDBOptions options) + private static CosmosDBExtensionConfigProvider CreateExtensionConfigProvider(CosmosDBOptions options, IConfiguration config = null) { - return CreateExtensionConfigProvider(new DefaultCosmosDBServiceFactory(), options); + return CreateExtensionConfigProvider(new DefaultCosmosDBServiceFactory(config ?? _baseConfig, Mock.Of()), options); } - private static CosmosDBExtensionConfigProvider CreateExtensionConfigProvider(ICosmosDBServiceFactory serviceFactory, CosmosDBOptions options) + private static CosmosDBExtensionConfigProvider CreateExtensionConfigProvider(ICosmosDBServiceFactory serviceFactory, CosmosDBOptions options, IConfiguration config = null) { - return new CosmosDBExtensionConfigProvider(new OptionsWrapper(options), serviceFactory, new DefaultCosmosDBSerializerFactory(), _emptyConfig, new TestNameResolver(), NullLoggerFactory.Instance); + return new CosmosDBExtensionConfigProvider(new OptionsWrapper(options), serviceFactory, new DefaultCosmosDBSerializerFactory(), new TestNameResolver(), NullLoggerFactory.Instance); } // These will use the default for ConnectionStringSetting, but override LeaseConnectionStringSetting