From 1f1cc0565dc3bf31d17e26d6497c0c4de2c54710 Mon Sep 17 00:00:00 2001 From: Bill DeRusha <444835+bderusha@users.noreply.github.com> Date: Fri, 23 Jun 2023 13:51:27 -0400 Subject: [PATCH] Implement Dynamic DataSource registration Signed-off-by: Bill DeRusha <444835+bderusha@users.noreply.github.com> --- .vscode/launch.json | 3 +- docs/architecture/data-sources.md | 59 ++---- docs/architecture/overview.md | 21 +- docs/packaging.md | 9 +- .../CarbonAware.CLI.IntegrationTests.csproj | 2 - .../Emissions/EmissionsCommandTests.cs | 1 - .../EmissionsForecastsCommandTests.cs | 3 +- .../Commands/Location/LocationCommandTests.cs | 3 +- .../IntegrationTestingBase.cs | 12 +- ...onAware.DataSources.ElectricityMaps.csproj | 1 - .../ServiceCollectionExtensions.cs | 56 ----- .../src/ElectricityMapsDataSource.cs | 51 ++++- .../ServiceCollectionExtensionTests.cs | 81 ------- .../test/ElectricityMapsDataSourceTests.cs | 86 ++++++++ ...taSources.ElectricityMapsFree.Mocks.csproj | 1 + .../ElectricityMapsFreeDataSourceMocker.cs | 16 +- ...are.DataSources.ElectricityMapsFree.csproj | 1 - .../ServiceCollectionExtensions.cs | 50 ----- .../src/ElectricityMapsFreeDataSource.cs | 50 +++++ .../src/CarbonAware.DataSources.Json.csproj | 1 - .../ServiceCollectionExtensions.cs | 20 -- .../src/JsonDataSource.cs | 27 ++- ...arbonAware.DataSources.Registration.csproj | 26 --- .../Configuration/DataSourceType.cs | 10 - .../ServiceCollectionExtensions.cs | 99 --------- .../CarbonAware.DataSources.WattTime.csproj | 1 - .../ServiceCollectionExtensions.cs | 62 ------ .../src/WattTimeDataSource.cs | 54 ++++- .../ServiceCollectionExtensionTests.cs | 198 +++++++++--------- .../WattTimeClientConfigurationTests.cs | 3 +- .../test/WattTimeDataSourceTests.cs | 89 +++++++- .../Properties/launchSettings.json | 8 + src/CarbonAware.WebApi/src/Program.cs | 2 +- ...CarbonAware.WebApi.IntegrationTests.csproj | 4 +- .../CarbonAwareControllerTests.cs | 15 +- .../IntegrationTestingBase.cs | 23 +- .../LocationsControllerTests.cs | 1 - .../UnconfiguredWebApiTests.cs | 34 --- src/CarbonAware/src/CarbonAware.csproj | 1 - .../Configuration/DataSourcesConfiguration.cs | 51 ++++- .../DataSourcesConfigurationExtensions.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 80 +++++++ src/CarbonAware/src/Interfaces/IDataSource.cs | 12 ++ .../src/Interfaces/IEmissionsDataSource.cs | 2 +- .../src/Interfaces/IForecastDataSource.cs | 2 +- .../DataSourcesConfigurationTests.cs | 6 +- src/CarbonAwareSDK.sln | 6 - .../ServiceCollectionExtensions.cs | 6 +- .../src/GSF.CarbonAware.csproj | 11 +- .../ServiceCollectionExtensionsTests.cs | 2 +- .../test/Handlers/EmissionsHandlerTests.cs | 6 +- 51 files changed, 698 insertions(+), 672 deletions(-) delete mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/Configuration/ServiceCollectionExtensions.cs delete mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/Configuration/ServiceCollectionExtensionTests.cs delete mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Configuration/ServiceCollectionExtensions.cs delete mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/Configuration/ServiceCollectionExtensions.cs delete mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj delete mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/DataSourceType.cs delete mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/ServiceCollectionExtensions.cs delete mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs create mode 100644 src/CarbonAware.Tools/CarbonAware.Tools.AzureRegionTestDataGenerator/Properties/launchSettings.json delete mode 100644 src/CarbonAware.WebApi/test/integrationTests/UnconfiguredWebApiTests.cs create mode 100644 src/CarbonAware/src/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/CarbonAware/src/Interfaces/IDataSource.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index 7df71697f..e5b060d98 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,7 +37,8 @@ "uriFormat": "%s/swagger" }, "env": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE": "false" }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" diff --git a/docs/architecture/data-sources.md b/docs/architecture/data-sources.md index e0e31acff..9c0bbbe3d 100644 --- a/docs/architecture/data-sources.md +++ b/docs/architecture/data-sources.md @@ -100,56 +100,28 @@ public class MyNewDataSource: IEmissionsDataSource ### Add Dependency Injection Configuration -The SDK uses dependency injection to load registered data sources based on set -environment variables. For a data source to be registered, it need to have a -Service Collection Extension defined. To do so, add a `Configuration` directory -in your data source project and create a new ServiceCollectionExtensions file. -We have provided a command snippet below: - -```sh -cd src/CarbonAware.DataSources/CarbonAware.DataSources.MyNewDataSource/src -mkdir Configuration -touch Configuration\ServiceCollectionExtensions.cs -``` - -Using the skeleton below, add the data source specific configuration and -implementation instances to the service collection. +The SDK uses dependency injection to load registered data sources based on configuration. For a data source to be registered, it needs to have a +static method `ConfigureDI` defined. We have provided an example below: ```csharp -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -namespace CarbonAware.DataSources.MyNewDataSource.Configuration; -public static class ServiceCollectionExtensions +public static IServiceCollection ConfigureDI(IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) + where T : IDataSource { - public static void AddMyNewDataSource(this IServiceCollection services) + var configSection = dataSourcesConfig.ConfigurationSection(); + AddMyNewDataSourceClient(services, configSection); + services.AddScoped(); + try + { + services.TryAddSingleton(typeof(T), typeof(MyNewDataSource)); + } catch (Exception ex) { - // ... register your data source with the IServiceCollection instance + throw new ArgumentException($"MyNewDataSource is not a supported {typeof(T).Name} data source.", ex); } + return services; } ``` -### Register the New Data Source - -Once the data source's ServiceCollectionExtensions is configured, it can be -registered as an available data source for the SDK by adding to the switch -statement found in the AddDataSourceService function of -[this file](../../src/CarbonAware.DataSources/CarbonAware.DataSources.Registration\Configuration\ServiceCollectionExtensions.cs). -Note you will need to add a new enum type to the `DataSourceType` -[enum file](../../src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/DataSourceType.cs) -to reference in the switch statement. - -```csharp - switch (dataSourceType) - { - ... - case DataSourceType.MyNewDataSourceEnum: - { - services.AddMyNewDataSource(); - break; - } - ... - } -``` +This function will be called at runtime to configure your data source like `MyNewDataSource.ConfigureDI(services, config);`. For more examples, check out the [implementations of the existing data sources](/src/CarbonAware.DataSources/). ### Adding Tests @@ -180,7 +152,8 @@ setting: ```bash DataSources__EmissionsDataSource="MyNewDataSource" -DataSources__Configurations__MyNewDataSource__Proxy__UseProxy=true +DataSources__Configurations__MyNewDataSource__Username="MyNewDataSourceUser123" +DataSources__Configurations__MyNewDataSource__Password="MyNewDataSourceP@ssword!" ``` Both the WebAPI and the CLI read the env variables in so once set, you can spin diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index d01969621..463e390b8 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -84,19 +84,14 @@ result. See the [data source README](./data-sources.md) for more detailed information. -## Dependency Registration - -The SDK uses dependency injection to load the data sources based on set -environment variables. To register a new dependency, a new -ServiceCollectionExtension method must be defined. These dependencies are loaded -in a hierarchical structure such that: - -1. Each data source defines a `ServiceCollectionExtension` method. -2. All available data sources are registered in the `DataSource.Registration` - project. -3. The GSF library defines a `ServiceCollectionExtension` method where it - registers the data sources for the handlers to use. -4. The `Program.cs` file registers the GSF library classes at startup +### Dependency Registration + +The SDK uses dependency injection to load the data sources based on configuration. To register a new dependency, the data source musr define a static method `ConfigureDI`. These dependencies are then loaded in the following manner: + +1. Each data source defines a `ConfigureDI` method. +2. The GSF library defines a `ServiceCollectionExtension` method where it + uses the configuration settings to dynamically load and configure the user-specified data sources for the handlers to use. +3. The `Program.cs` file registers the GSF library classes at startup ## Example Call Flow diff --git a/docs/packaging.md b/docs/packaging.md index c8977a313..0ae684e22 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -20,18 +20,17 @@ showing how the package can be consumed. ## Included Projects -The current package include 8 projects from the SDK: +The current package include 7 projects from the SDK: 1. "GSF.CarbonAware" 2. "CarbonAware" 3. "CarbonAware.DataSources.ElectricityMapsFree" 4. "CarbonAware.DataSources.ElectricityMaps" 5. "CarbonAware.DataSources.Json" -6. "CarbonAware.DataSources.Registration" -7. "CarbonAware.DataSources.WattTime" -8. "CarbonAware.LocationSources" +6. "CarbonAware.DataSources.WattTime" +7. "CarbonAware.LocationSources" -These 8 projects enable users of the library to consume the current endpoints +These 7 projects enable users of the library to consume the current endpoints exposed by the library. The package that needs to be added to a new C# project is `GSF.CarbonAware`. diff --git a/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj b/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj index 9b0dd0efe..a0859cea7 100644 --- a/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj +++ b/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj @@ -15,8 +15,6 @@ - /// A base class that does all the common setup for the Integration Testing /// Overrides WebAPI factory by switching out different configurations via _datasource diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/CarbonAware.DataSources.ElectricityMaps.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/CarbonAware.DataSources.ElectricityMaps.csproj index d9dab5e3e..a8758481a 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/CarbonAware.DataSources.ElectricityMaps.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/CarbonAware.DataSources.ElectricityMaps.csproj @@ -10,7 +10,6 @@ - diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/Configuration/ServiceCollectionExtensions.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/Configuration/ServiceCollectionExtensions.cs deleted file mode 100644 index c1a036977..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/Configuration/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using CarbonAware.Configuration; -using CarbonAware.Interfaces; -using CarbonAware.DataSources.ElectricityMaps.Client; -using CarbonAware.Exceptions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using System.Net; - -namespace CarbonAware.DataSources.ElectricityMaps.Configuration; - -internal static class ServiceCollectionExtensions -{ - public static IServiceCollection AddElectricityMapsForecastDataSource(this IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) - { - AddElectricityMapsClient(services, dataSourcesConfig.ForecastConfigurationSection()); - services.TryAddSingleton(); - return services; - } - - public static IServiceCollection AddElectricityMapsEmissionsDataSource(this IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) - { - AddElectricityMapsClient(services, dataSourcesConfig.EmissionsConfigurationSection()); - services.TryAddSingleton(); - return services; - } - - private static void AddElectricityMapsClient(IServiceCollection services, IConfigurationSection configSection) - { - services.Configure(c => - { - configSection.Bind(c); - }); - - var httpClientBuilder = services.AddHttpClient(IElectricityMapsClient.NamedClient); - - var Proxy = configSection.GetSection("Proxy").Get(); - if (Proxy?.UseProxy == true) - { - if (String.IsNullOrEmpty(Proxy.Url)) - { - throw new ConfigurationException("Proxy Url is not configured."); - } - httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => - new HttpClientHandler() { - Proxy = new WebProxy { - Address = new Uri(Proxy.Url), - Credentials = new NetworkCredential(Proxy.Username, Proxy.Password), - BypassProxyOnLocal = true - } - } - ); - } - services.TryAddSingleton(); - } -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs index 01e494bbd..3519aa0e2 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs @@ -1,10 +1,15 @@ +using CarbonAware.Configuration; using CarbonAware.DataSources.ElectricityMaps.Client; +using CarbonAware.DataSources.ElectricityMaps.Configuration; using CarbonAware.DataSources.ElectricityMaps.Model; using CarbonAware.Exceptions; using CarbonAware.Interfaces; using CarbonAware.Model; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; -using System.Diagnostics; +using System.Net; namespace CarbonAware.DataSources.ElectricityMaps; @@ -40,6 +45,21 @@ public ElectricityMapsDataSource(ILogger logger, IEle this._locationSource = locationSource; } + public static IServiceCollection ConfigureDI(IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) + where T : IDataSource + { + var configSection = dataSourcesConfig.ConfigurationSection(); + AddElectricityMapsClient(services, configSection); + try + { + services.TryAddSingleton(typeof(T), typeof(ElectricityMapsDataSource)); + } catch (Exception ex) + { + throw new ArgumentException($"ElectricityMapsDataSource is not a supported {typeof(T).Name} data source.", ex); + } + return services; + } + /// public async Task GetCurrentCarbonIntensityForecastAsync(Location location) { @@ -176,4 +196,33 @@ private TimeSpan GetDurationFromHistoryDataPoints(IEnumerable d // the absolute value of the TimeSpan between the two points. return first.DateTime.Subtract(second.DateTime).Duration(); } + + private static void AddElectricityMapsClient(IServiceCollection services, IConfigurationSection configSection) + { + services.Configure(c => + { + configSection.Bind(c); + }); + + var httpClientBuilder = services.AddHttpClient(IElectricityMapsClient.NamedClient); + + var Proxy = configSection.GetSection("Proxy").Get(); + if (Proxy?.UseProxy == true) + { + if (String.IsNullOrEmpty(Proxy.Url)) + { + throw new ConfigurationException("Proxy Url is not configured."); + } + httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => + new HttpClientHandler() { + Proxy = new WebProxy { + Address = new Uri(Proxy.Url), + Credentials = new NetworkCredential(Proxy.Username, Proxy.Password), + BypassProxyOnLocal = true + } + } + ); + } + services.TryAddSingleton(); + } } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/Configuration/ServiceCollectionExtensionTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/Configuration/ServiceCollectionExtensionTests.cs deleted file mode 100644 index 8fa70908e..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/Configuration/ServiceCollectionExtensionTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -using CarbonAware.Configuration; -using CarbonAware.DataSources.ElectricityMaps.Configuration; -using CarbonAware.Exceptions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace CarbonAware.DataSources.ElectricityMaps.Tests; - -[TestFixture] -class ServiceCollectionExtensionTests -{ - const string ForecastDataSourceKey = $"DataSources:ForecastDataSource"; - const string EmissionsDataSourceKey = $"DataSources:EmissionsDataSource"; - const string ForecastDataSourceValue = $"ElectricityMapsTest"; - const string EmissionsDataSourceValue = $"ElectricityMapsTest"; - const string HeaderKey = $"DataSources:Configurations:ElectricityMapsTest:APITokenHeader"; - const string AuthHeader = "auth-token"; - const string TokenKey = $"DataSources:Configurations:ElectricityMapsTest:APIToken"; - const string DefaultTokenValue = "myDefaultToken123"; - const string UseProxyKey = $"DataSources:Configurations:ElectricityMapsTest:Proxy:UseProxy"; - const string ProxyUrl = $"DataSources:Configurations:ElectricityMapsTest:Proxy:Url"; - const string ProxyUsername = $"DataSources:Configurations:ElectricityMapsTest:Proxy:Username"; - const string ProxyPassword = $"DataSources:Configurations:ElectricityMapsTest:Proxy:Password"; - - [Test] - public void ClientProxyTest_With_Missing_ProxyURL_ThrowsException() - { - // Arrange - var settings = new Dictionary { - { ForecastDataSourceKey, ForecastDataSourceValue }, - { EmissionsDataSourceKey, EmissionsDataSourceValue }, - { HeaderKey, AuthHeader }, - { TokenKey, DefaultTokenValue }, - { UseProxyKey, "true" }, - }; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(settings) - .AddEnvironmentVariables() - .Build(); - var serviceCollection = new ServiceCollection(); - - // Act & Assert - Assert.Throws(() => serviceCollection.AddElectricityMapsForecastDataSource(configuration.DataSources())); - Assert.Throws(() => serviceCollection.AddElectricityMapsEmissionsDataSource(configuration.DataSources())); - } - - [TestCase(true, TestName = "ClientProxyTest, successful: denotes adding ElectricityMaps data sources using proxy.")] - [TestCase(false, TestName = "ClientProxyTest, successful: denotes adding ElectricityMaps data sources without using proxy.")] - public void ClientProxy_ConfigurationTest(bool withProxyUrl) - { - // Arrange - var settings = new Dictionary { - { ForecastDataSourceKey, ForecastDataSourceValue }, - { EmissionsDataSourceKey, EmissionsDataSourceValue }, - { HeaderKey, AuthHeader }, - { TokenKey, DefaultTokenValue }, - { UseProxyKey, withProxyUrl.ToString() } - }; - - if (withProxyUrl) - { - settings.Add(ProxyUrl, "http://fakeProxy"); - settings.Add(ProxyUsername, "proxyUsername"); - settings.Add(ProxyPassword, "proxyPassword"); - } - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(settings) - .AddEnvironmentVariables() - .Build(); - var serviceCollection = new ServiceCollection(); - - // Act - var forecastDataSource = serviceCollection.AddElectricityMapsForecastDataSource(configuration.DataSources()); - var emissionsDataSource = serviceCollection.AddElectricityMapsEmissionsDataSource(configuration.DataSources()); - - // Assert - Assert.That(forecastDataSource, Is.Not.Null); - Assert.That(emissionsDataSource, Is.Not.Null); - } -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/ElectricityMapsDataSourceTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/ElectricityMapsDataSourceTests.cs index ff1dcc56a..77025d52f 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/ElectricityMapsDataSourceTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/ElectricityMapsDataSourceTests.cs @@ -1,8 +1,11 @@ +using CarbonAware.Configuration; using CarbonAware.DataSources.ElectricityMaps.Client; using CarbonAware.DataSources.ElectricityMaps.Model; using CarbonAware.Exceptions; using CarbonAware.Interfaces; using CarbonAware.Model; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; @@ -23,6 +26,14 @@ class ElectricityMapsDataSourceTests private static string _defaultLongitude => _defaultLocation.Longitude.ToString() ?? ""; private static DateTimeOffset _defaultDataStartTime = new DateTimeOffset(2022, 4, 18, 12, 32, 42, TimeSpan.FromHours(-6)); + private readonly string TypeKey = $"Configurations:ElectricityMaps:Type"; + private readonly string UsernameKey = $"Configurations:ElectricityMaps:Username"; + private readonly string PasswordKey = $"Configurations:ElectricityMaps:Password"; + private readonly string UseProxyKey = $"Configurations:ElectricityMaps:Proxy:UseProxy"; + private readonly string ProxyUrlKey = $"Configurations:ElectricityMaps:Proxy:Url"; + private readonly string ProxyUsernameKey = $"Configurations:ElectricityMaps:Proxy:Username"; + private readonly string ProxyPasswordKey = $"Configurations:ElectricityMaps:Proxy:Password"; + [SetUp] public void Setup() { @@ -324,4 +335,79 @@ public async Task GetDurationBetweenHistoryDataPoints_WhenMultipleDataPoints_Ret Assert.IsNotNull(second); Assert.That(second.Duration, Is.EqualTo(expectedDuration)); } + + [Test] + public void ConfigureDI_ClientProxyTest_With_Missing_ProxyURL_ThrowsException() + { + // Arrange + var inMemorySettings = new Dictionary { + { TypeKey, "ElectricityMaps" }, + { UsernameKey, "testUsername" }, + { PasswordKey, "testPassword123!" }, + { UseProxyKey, "true" }, + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + var dataSourceConfig = new DataSourcesConfiguration() + { + EmissionsDataSource = "ElectricityMaps", + ForecastDataSource = "ElectricityMaps", + Section = configuration.GetSection("Configurations") + }; + + var serviceCollection = new ServiceCollection(); + + // Act & Assert + Assert.Throws(() => ElectricityMapsDataSource.ConfigureDI(serviceCollection, dataSourceConfig)); + Assert.Throws(() => ElectricityMapsDataSource.ConfigureDI(serviceCollection, dataSourceConfig)); + } + + [TestCase(true, TestName = "ClientProxyTest, successful: denotes adding ElectricityMaps data sources using proxy.")] + [TestCase(false, TestName = "ClientProxyTest, successful: denotes adding ElectricityMaps data sources without using proxy.")] + public void ConfigureDI_ClientProxyTest_AddsDataSource(bool withProxyUrl) + { + // Arrange + var inMemorySettings = new Dictionary { + { TypeKey, "ElectricityMaps" }, + { UsernameKey, "testUsername" }, + { PasswordKey, "testPassword123!" }, + { UseProxyKey, withProxyUrl.ToString() }, + }; + + if (withProxyUrl) + { + inMemorySettings.Add(ProxyUrlKey, "http://10.10.10.1"); + inMemorySettings.Add(ProxyUsernameKey, "proxyUsername"); + inMemorySettings.Add(ProxyPasswordKey, "proxyPassword"); + } + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + var dataSourceConfig = new DataSourcesConfiguration() + { + EmissionsDataSource = "ElectricityMaps", + ForecastDataSource = "ElectricityMaps", + Section = configuration.GetSection("Configurations") + }; + + var serviceCollection = new ServiceCollection(); + var emissionsDescriptor = new ServiceDescriptor(typeof(IEmissionsDataSource), typeof(ElectricityMapsDataSource), ServiceLifetime.Singleton); + var forecastDescriptor = new ServiceDescriptor(typeof(IForecastDataSource), typeof(ElectricityMapsDataSource), ServiceLifetime.Singleton); + + Assert.That(!serviceCollection.Any(i => i.ToString() == emissionsDescriptor.ToString())); + Assert.That(!serviceCollection.Any(i => i.ToString() == forecastDescriptor.ToString())); + + // Act + ElectricityMapsDataSource.ConfigureDI(serviceCollection, dataSourceConfig); + ElectricityMapsDataSource.ConfigureDI(serviceCollection, dataSourceConfig); + + // Assert + Assert.That(serviceCollection.Any(i => i.ToString() == emissionsDescriptor.ToString())); + Assert.That(serviceCollection.Any(i => i.ToString() == forecastDescriptor.ToString())); + } } \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj index 60a945672..ae28cd4a8 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj @@ -16,6 +16,7 @@ + diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/ElectricityMapsFreeDataSourceMocker.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/ElectricityMapsFreeDataSourceMocker.cs index f8a645aca..92d6253ce 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/ElectricityMapsFreeDataSourceMocker.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/ElectricityMapsFreeDataSourceMocker.cs @@ -44,21 +44,11 @@ public void SetupDataMock(DateTimeOffset start, DateTimeOffset end, string locat SetupResponseGivenGetRequest(Paths.Latest, data); } - public void SetupForecastMock() - { - throw new NotImplementedException(); - } + public void SetupForecastMock() { } - public void SetupBatchForecastMock() - { - throw new NotImplementedException(); - } + public void SetupBatchForecastMock() { } - public void Initialize() - { - // No initialization needed - return; - } + public void Initialize() { } public void Reset() => _server.Reset(); diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/CarbonAware.DataSources.ElectricityMapsFree.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/CarbonAware.DataSources.ElectricityMapsFree.csproj index 8db8bc27b..b3a907bc4 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/CarbonAware.DataSources.ElectricityMapsFree.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/CarbonAware.DataSources.ElectricityMapsFree.csproj @@ -8,7 +8,6 @@ - diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Configuration/ServiceCollectionExtensions.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Configuration/ServiceCollectionExtensions.cs deleted file mode 100644 index 8156b2b87..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Configuration/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -using CarbonAware.Configuration; -using CarbonAware.DataSources.ElectricityMapsFree.Client; -using CarbonAware.Exceptions; -using CarbonAware.Interfaces; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using System.Net; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Configuration; - -internal static class ServiceCollectionExtensions -{ - public static IServiceCollection AddElectricityMapsFreeEmissionsDataSource(this IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) - { - AddElectricityMapsFreeClient(services, dataSourcesConfig.EmissionsConfigurationSection()); - services.TryAddSingleton(); - return services; - } - - - private static void AddElectricityMapsFreeClient(IServiceCollection services, IConfigurationSection configSection) - { - services.Configure(c => - { - configSection.Bind(c); - }); - - var httpClientBuilder = services.AddHttpClient(IElectricityMapsFreeClient.NamedClient); - - var Proxy = configSection.GetSection("Proxy").Get(); - if (Proxy?.UseProxy == true) - { - if (String.IsNullOrEmpty(Proxy.Url)) - { - throw new ConfigurationException("Proxy Url is not configured."); - } - httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => - new HttpClientHandler() { - Proxy = new WebProxy { - Address = new Uri(Proxy.Url), - Credentials = new NetworkCredential(Proxy.Username, Proxy.Password), - BypassProxyOnLocal = true - } - } - ); - } - services.TryAddSingleton(); - } -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/ElectricityMapsFreeDataSource.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/ElectricityMapsFreeDataSource.cs index 60ad2a61c..1aff0174f 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/ElectricityMapsFreeDataSource.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/ElectricityMapsFreeDataSource.cs @@ -1,9 +1,16 @@ using CarbonAware.DataSources.ElectricityMapsFree.Client; +using CarbonAware.DataSources.ElectricityMapsFree.Configuration; using CarbonAware.DataSources.ElectricityMapsFree.Model; +using CarbonAware.Exceptions; using CarbonAware.Interfaces; using CarbonAware.Model; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; +using System.Net; using System.Diagnostics; +using CarbonAware.Configuration; namespace CarbonAware.DataSources.ElectricityMapsFree; /// @@ -40,6 +47,20 @@ public ElectricityMapsFreeDataSource(ILogger logg this._locationSource = locationSource; } + public static IServiceCollection ConfigureDI(IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) + where T : IDataSource + { + var configSection = dataSourcesConfig.ConfigurationSection(); + AddElectricityMapsFreeClient(services, configSection); + try + { + services.TryAddSingleton(typeof(T), typeof(ElectricityMapsFreeDataSource)); + } catch (Exception ex) + { + throw new ArgumentException($"ElectricityMapsFreeDataSource is not a supported {typeof(T).Name} data source.", ex); + } + return services; + } /// public async Task> GetCarbonIntensityAsync(IEnumerable locations, DateTimeOffset periodStartTime, DateTimeOffset periodEndTime) @@ -98,4 +119,33 @@ public async Task> GetCarbonIntensityAsync(Location l return EmissionsDataList; } + + private static void AddElectricityMapsFreeClient(IServiceCollection services, IConfigurationSection configSection) + { + services.Configure(c => + { + configSection.Bind(c); + }); + + var httpClientBuilder = services.AddHttpClient(IElectricityMapsFreeClient.NamedClient); + + var Proxy = configSection.GetSection("Proxy").Get(); + if (Proxy?.UseProxy == true) + { + if (String.IsNullOrEmpty(Proxy.Url)) + { + throw new ConfigurationException("Proxy Url is not configured."); + } + httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => + new HttpClientHandler() { + Proxy = new WebProxy { + Address = new Uri(Proxy.Url), + Credentials = new NetworkCredential(Proxy.Username, Proxy.Password), + BypassProxyOnLocal = true + } + } + ); + } + services.TryAddSingleton(); + } } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj index 7c9905d41..1d0002f72 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj @@ -17,7 +17,6 @@ - diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/Configuration/ServiceCollectionExtensions.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/Configuration/ServiceCollectionExtensions.cs deleted file mode 100644 index edde1f037..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/Configuration/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using CarbonAware.Configuration; -using CarbonAware.Interfaces; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace CarbonAware.DataSources.Json.Configuration; - -internal static class ServiceCollectionExtensions -{ - public static void AddJsonEmissionsDataSource(this IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) - { - // configuring dependency injection to have config. - services.Configure(config => - { - dataSourcesConfig.EmissionsConfigurationSection().Bind(config); - }); - services.TryAddSingleton(); - } -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/JsonDataSource.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/JsonDataSource.cs index c62735dcb..63fc7f2f7 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/JsonDataSource.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/JsonDataSource.cs @@ -1,8 +1,14 @@ -using CarbonAware.Interfaces; +using CarbonAware.Configuration; using CarbonAware.DataSources.Json.Configuration; +using CarbonAware.Exceptions; +using CarbonAware.Interfaces; using CarbonAware.Model; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using System.Net; using System.Text.Json; namespace CarbonAware.DataSources.Json; @@ -42,6 +48,25 @@ public JsonDataSource(ILogger logger, IOptionsMonitor(IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) + where T : IDataSource + { + var configSection = dataSourcesConfig.ConfigurationSection(); + services.Configure(config => + { + configSection.Bind(config); + }); + + try + { + services.TryAddSingleton(typeof(T), typeof(JsonDataSource)); + } catch (Exception ex) + { + throw new ArgumentException($"JsonDataSource is not a supported {typeof(T).Name} data source.", ex); + } + return services; + } + /// public Task> GetCarbonIntensityAsync(Location location, DateTimeOffset periodStartTime, DateTimeOffset periodEndTime) { diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj deleted file mode 100644 index d66d5e8c7..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - net6.0 - enable - enable - true - false - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/DataSourceType.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/DataSourceType.cs deleted file mode 100644 index a1d93e1d7..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/DataSourceType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CarbonAware.DataSources.Configuration; - -public enum DataSourceType -{ - None, - WattTime, - JSON, - ElectricityMaps, - ElectricityMapsFree, -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/ServiceCollectionExtensions.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/ServiceCollectionExtensions.cs deleted file mode 100644 index 872ec4bae..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,99 +0,0 @@ -using CarbonAware.Configuration; -using CarbonAware.Interfaces; -using CarbonAware.DataSources.ElectricityMaps.Configuration; -using CarbonAware.DataSources.ElectricityMapsFree.Configuration; -using CarbonAware.DataSources.Json.Configuration; -using CarbonAware.DataSources.WattTime.Configuration; -using CarbonAware.Exceptions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace CarbonAware.DataSources.Configuration; -internal static class ServiceCollectionExtensions -{ - public static IServiceCollection AddDataSourceService(this IServiceCollection services, IConfiguration configuration) - { - var dataSources = configuration.DataSources(); - - var emissionsDataSource = GetDataSourceTypeFromValue(dataSources.EmissionsConfigurationType()); - var forecastDataSource = GetDataSourceTypeFromValue(dataSources.ForecastConfigurationType()); - - switch (emissionsDataSource) - { - case DataSourceType.JSON: - { - services.AddJsonEmissionsDataSource(dataSources); - break; - } - case DataSourceType.WattTime: - { - services.AddWattTimeEmissionsDataSource(dataSources); - break; - } - case DataSourceType.ElectricityMaps: - { - services.AddElectricityMapsEmissionsDataSource(dataSources); - break; - } - case DataSourceType.ElectricityMapsFree: - { - services.AddElectricityMapsFreeEmissionsDataSource(dataSources); - break; - } - case DataSourceType.None: - { - services.TryAddSingleton(); - break; - } - } - - switch (forecastDataSource) - { - case DataSourceType.JSON: - { - throw new ArgumentException("JSON data source is not supported for forecast data"); - } - case DataSourceType.WattTime: - { - services.AddWattTimeForecastDataSource(dataSources); - break; - } - case DataSourceType.ElectricityMaps: - { - services.AddElectricityMapsForecastDataSource(dataSources); - break; - } - case DataSourceType.ElectricityMapsFree: - { - throw new ArgumentException("ElectricityMapsFree data source is not supported for forecast data"); - } - case DataSourceType.None: - { - services.TryAddSingleton(); - break; - } - } - - if (forecastDataSource == DataSourceType.None && emissionsDataSource == DataSourceType.None) - { - throw new ConfigurationException("No data sources are configured"); - } - - return services; - } - - private static DataSourceType GetDataSourceTypeFromValue(string? srcVal) - { - DataSourceType result; - if (String.IsNullOrWhiteSpace(srcVal)) - { - result = DataSourceType.None; - } - else if (!Enum.TryParse(srcVal, true, out result)) - { - throw new ArgumentException($"Unknown data source type: {srcVal}"); - } - return result; - } -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/CarbonAware.DataSources.WattTime.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/CarbonAware.DataSources.WattTime.csproj index 20f6d7230..f9f3e6bcc 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/CarbonAware.DataSources.WattTime.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/CarbonAware.DataSources.WattTime.csproj @@ -9,7 +9,6 @@ - diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs deleted file mode 100644 index b3ba9d358..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -using CarbonAware.Configuration; -using CarbonAware.DataSources.WattTime.Client; -using CarbonAware.Exceptions; -using CarbonAware.Interfaces; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using System.Net; - -namespace CarbonAware.DataSources.WattTime.Configuration; - -internal static class ServiceCollectionExtensions -{ - public static IServiceCollection AddWattTimeForecastDataSource(this IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) - { - AddDependencies(services, dataSourcesConfig.ForecastConfigurationSection()); - services.TryAddSingleton(); - return services; - } - - public static IServiceCollection AddWattTimeEmissionsDataSource(this IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) - { - AddDependencies(services, dataSourcesConfig.EmissionsConfigurationSection()); - services.TryAddSingleton(); - return services; - } - - private static void AddDependencies(IServiceCollection services, IConfigurationSection configSection) - { - AddWattTimeClient(services, configSection); - services.AddMemoryCache(); - } - - private static void AddWattTimeClient(IServiceCollection services, IConfigurationSection configSection) - { - services.Configure(c => - { - configSection.Bind(c); - }); - - var httpClientBuilder = services.AddHttpClient(IWattTimeClient.NamedClient); - - var Proxy = configSection.GetSection("Proxy").Get(); - if (Proxy != null && Proxy.UseProxy == true) - { - if (String.IsNullOrEmpty(Proxy.Url)) - { - throw new Exceptions.ConfigurationException("Proxy Url is not configured."); - } - httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => - new HttpClientHandler() { - Proxy = new WebProxy { - Address = new Uri(Proxy.Url), - Credentials = new NetworkCredential(Proxy.Username, Proxy.Password), - BypassProxyOnLocal = true - } - } - ); - } - services.TryAddSingleton(); - } -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs index b639bd2e7..4c7e9a758 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs @@ -1,10 +1,15 @@ -using CarbonAware.DataSources.WattTime.Client; +using CarbonAware.Configuration; +using CarbonAware.DataSources.WattTime.Client; +using CarbonAware.DataSources.WattTime.Configuration; using CarbonAware.DataSources.WattTime.Model; using CarbonAware.Exceptions; using CarbonAware.Interfaces; using CarbonAware.Model; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; -using System.Diagnostics; +using System.Net; namespace CarbonAware.DataSources.WattTime; @@ -44,6 +49,22 @@ public WattTimeDataSource(ILogger logger, IWattTimeClient cl this.LocationSource = locationSource; } + public static IServiceCollection ConfigureDI(IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) + where T : IDataSource + { + var configSection = dataSourcesConfig.ConfigurationSection(); + AddWattTimeClient(services, configSection); + services.AddMemoryCache(); + try + { + services.TryAddSingleton(typeof(T), typeof(WattTimeDataSource)); + } catch (Exception ex) + { + throw new ArgumentException($"WattTimeDataSource is not a supported {typeof(T).Name} data source.", ex); + } + return services; + } + /// public async Task> GetCarbonIntensityAsync(IEnumerable locations, DateTimeOffset periodStartTime, DateTimeOffset periodEndTime) { @@ -196,4 +217,33 @@ private DateTimeOffset TimeToLowestIncrement(DateTimeOffset date, int minutes = var d = TimeSpan.FromMinutes(minutes); return new DateTimeOffset((date.Ticks / d.Ticks) * d.Ticks, date.Offset); } + + private static void AddWattTimeClient(IServiceCollection services, IConfigurationSection configSection) + { + services.Configure(c => + { + configSection.Bind(c); + }); + + var httpClientBuilder = services.AddHttpClient(IWattTimeClient.NamedClient); + + var Proxy = configSection.GetSection("Proxy").Get(); + if (Proxy != null && Proxy.UseProxy == true) + { + if (String.IsNullOrEmpty(Proxy.Url)) + { + throw new Exceptions.ConfigurationException("Proxy Url is not configured."); + } + httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => + new HttpClientHandler() { + Proxy = new WebProxy { + Address = new Uri(Proxy.Url), + Credentials = new NetworkCredential(Proxy.Username, Proxy.Password), + BypassProxyOnLocal = true + } + } + ); + } + services.TryAddSingleton(); + } } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/ServiceCollectionExtensionTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/ServiceCollectionExtensionTests.cs index b3f845e3a..e6ddac947 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/ServiceCollectionExtensionTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/ServiceCollectionExtensionTests.cs @@ -1,112 +1,112 @@ -using CarbonAware.Configuration; -using CarbonAware.DataSources.WattTime.Client; -using CarbonAware.DataSources.WattTime.Configuration; -using CarbonAware.Exceptions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; -using System.Collections.Generic; -using System.Net.Http; +// using CarbonAware.Configuration; +// using CarbonAware.DataSources.WattTime.Client; +// using CarbonAware.DataSources.WattTime.Configuration; +// using CarbonAware.Exceptions; +// using Microsoft.Extensions.Configuration; +// using Microsoft.Extensions.DependencyInjection; +// using NUnit.Framework; +// using System.Collections.Generic; +// using System.Net.Http; -namespace CarbonAware.DataSources.WattTime.Tests; +// namespace CarbonAware.DataSources.WattTime.Tests; -[TestFixture] -class ServiceCollectionExtensionTests -{ - private readonly string ForecastDataSourceKey = $"DataSources:ForecastDataSource"; - private readonly string EmissionsDataSourceKey = $"DataSources:EmissionsDataSource"; - private readonly string ForecastDataSourceValue = $"WattTimeTest"; - private readonly string EmissionsDataSourceValue = $"WattTimeTest"; - private readonly string UsernameKey = $"DataSources:Configurations:WattTimeTest:Username"; - private readonly string Username = "devuser"; - private readonly string PasswordKey = $"DataSources:Configurations:WattTimeTest:Password"; - private readonly string Password = "12345"; - private readonly string ProxyUrl = $"DataSources:Configurations:WattTimeTest:Proxy:Url"; - private readonly string ProxyUsername = $"DataSources:Configurations:WattTimeTest:Proxy:Username"; - private readonly string ProxyPassword = $"DataSources:Configurations:WattTimeTest:Proxy:Password"; - private readonly string UseProxyKey = $"DataSources:Configurations:WattTimeTest:Proxy:UseProxy"; +// [TestFixture] +// class ServiceCollectionExtensionTests +// { +// private readonly string ForecastDataSourceKey = $"DataSources:ForecastDataSource"; +// private readonly string EmissionsDataSourceKey = $"DataSources:EmissionsDataSource"; +// private readonly string ForecastDataSourceValue = $"WattTimeTest"; +// private readonly string EmissionsDataSourceValue = $"WattTimeTest"; +// private readonly string UsernameKey = $"DataSources:Configurations:WattTimeTest:Username"; +// private readonly string Username = "devuser"; +// private readonly string PasswordKey = $"DataSources:Configurations:WattTimeTest:Password"; +// private readonly string Password = "12345"; +// private readonly string ProxyUrl = $"DataSources:Configurations:WattTimeTest:Proxy:Url"; +// private readonly string ProxyUsername = $"DataSources:Configurations:WattTimeTest:Proxy:Username"; +// private readonly string ProxyPassword = $"DataSources:Configurations:WattTimeTest:Proxy:Password"; +// private readonly string UseProxyKey = $"DataSources:Configurations:WattTimeTest:Proxy:UseProxy"; - [Test] - public void ClientProxyTest_With_Invalid_ProxyURL_ThrowsException() - { - // Arrange - var settings = new Dictionary { - { ForecastDataSourceKey, ForecastDataSourceValue }, - { EmissionsDataSourceKey, EmissionsDataSourceValue }, - { UsernameKey, Username }, - { PasswordKey, Password }, - { ProxyUrl, "http://fakeproxy:8080" }, - { UseProxyKey, "true" }, - }; +// [Test] +// public void ClientProxyTest_With_Invalid_ProxyURL_ThrowsException() +// { +// // Arrange +// var settings = new Dictionary { +// { ForecastDataSourceKey, ForecastDataSourceValue }, +// { EmissionsDataSourceKey, EmissionsDataSourceValue }, +// { UsernameKey, Username }, +// { PasswordKey, Password }, +// { ProxyUrl, "http://fakeproxy:8080" }, +// { UseProxyKey, "true" }, +// }; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(settings) - .AddEnvironmentVariables() - .Build(); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddWattTimeForecastDataSource(configuration.DataSources()); - var serviceProvider = serviceCollection.BuildServiceProvider(); - var client = serviceProvider.GetRequiredService(); +// var configuration = new ConfigurationBuilder() +// .AddInMemoryCollection(settings) +// .AddEnvironmentVariables() +// .Build(); +// var serviceCollection = new ServiceCollection(); +// serviceCollection.AddWattTimeForecastDataSource(configuration.DataSources()); +// var serviceProvider = serviceCollection.BuildServiceProvider(); +// var client = serviceProvider.GetRequiredService(); - // Act & Assert - Assert.ThrowsAsync(async () => await client.GetBalancingAuthorityAsync("lat", "long")); - } +// // Act & Assert +// Assert.ThrowsAsync(async () => await client.GetBalancingAuthorityAsync("lat", "long")); +// } - [Test] - public void ClientProxyTest_With_Missing_ProxyURL_ThrowsException() - { - // Arrange - var settings = new Dictionary { - { ForecastDataSourceKey, ForecastDataSourceValue }, - { EmissionsDataSourceKey, EmissionsDataSourceValue }, - { UsernameKey, Username }, - { PasswordKey, Password }, - { UseProxyKey, "true" }, - }; +// [Test] +// public void ClientProxyTest_With_Missing_ProxyURL_ThrowsException() +// { +// // Arrange +// var settings = new Dictionary { +// { ForecastDataSourceKey, ForecastDataSourceValue }, +// { EmissionsDataSourceKey, EmissionsDataSourceValue }, +// { UsernameKey, Username }, +// { PasswordKey, Password }, +// { UseProxyKey, "true" }, +// }; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(settings) - .AddEnvironmentVariables() - .Build(); - var serviceCollection = new ServiceCollection(); +// var configuration = new ConfigurationBuilder() +// .AddInMemoryCollection(settings) +// .AddEnvironmentVariables() +// .Build(); +// var serviceCollection = new ServiceCollection(); - // Act & Assert - Assert.Throws(() => serviceCollection.AddWattTimeForecastDataSource(configuration.DataSources())); - Assert.Throws(() => serviceCollection.AddWattTimeEmissionsDataSource(configuration.DataSources())); - } +// // Act & Assert +// Assert.Throws(() => serviceCollection.AddWattTimeForecastDataSource(configuration.DataSources())); +// Assert.Throws(() => serviceCollection.AddWattTimeEmissionsDataSource(configuration.DataSources())); +// } - [TestCase(true, TestName = "ClientProxyTest, successful: denotes adding WattTime data sources using proxy.")] - [TestCase(false, TestName = "ClientProxyTest, successful: denotes adding WattTime data sources without using proxy.")] - public void ClientProxyTest_AddsDataSource(bool withProxyUrl) - { - // Arrange - var settings = new Dictionary { - { ForecastDataSourceKey, ForecastDataSourceValue }, - { EmissionsDataSourceKey, EmissionsDataSourceValue }, - { UsernameKey, Username }, - { PasswordKey, Password }, - { UseProxyKey, withProxyUrl.ToString() } - }; +// [TestCase(true, TestName = "ClientProxyTest, successful: denotes adding WattTime data sources using proxy.")] +// [TestCase(false, TestName = "ClientProxyTest, successful: denotes adding WattTime data sources without using proxy.")] +// public void ClientProxyTest_AddsDataSource(bool withProxyUrl) +// { +// // Arrange +// var settings = new Dictionary { +// { ForecastDataSourceKey, ForecastDataSourceValue }, +// { EmissionsDataSourceKey, EmissionsDataSourceValue }, +// { UsernameKey, Username }, +// { PasswordKey, Password }, +// { UseProxyKey, withProxyUrl.ToString() } +// }; - if (withProxyUrl) - { - settings.Add(ProxyUrl, "http://10.10.10.1"); - settings.Add(ProxyUsername, "proxyUsername"); - settings.Add(ProxyPassword, "proxyPassword"); - } +// if (withProxyUrl) +// { +// settings.Add(ProxyUrl, "http://10.10.10.1"); +// settings.Add(ProxyUsername, "proxyUsername"); +// settings.Add(ProxyPassword, "proxyPassword"); +// } - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(settings) - .AddEnvironmentVariables() - .Build(); - var serviceCollection = new ServiceCollection(); +// var configuration = new ConfigurationBuilder() +// .AddInMemoryCollection(settings) +// .AddEnvironmentVariables() +// .Build(); +// var serviceCollection = new ServiceCollection(); - // Act - var forecastDataSource = serviceCollection.AddWattTimeEmissionsDataSource(configuration.DataSources()); - var emissionsDataSource = serviceCollection.AddWattTimeForecastDataSource(configuration.DataSources()); +// // Act +// var forecastDataSource = serviceCollection.AddWattTimeEmissionsDataSource(configuration.DataSources()); +// var emissionsDataSource = serviceCollection.AddWattTimeForecastDataSource(configuration.DataSources()); - // Assert - Assert.That(forecastDataSource, Is.Not.Null); - Assert.That(emissionsDataSource, Is.Not.Null); - } -} \ No newline at end of file +// // Assert +// Assert.That(forecastDataSource, Is.Not.Null); +// Assert.That(emissionsDataSource, Is.Not.Null); +// } +// } \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/WattTimeClientConfigurationTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/WattTimeClientConfigurationTests.cs index 69c666c40..008888d06 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/WattTimeClientConfigurationTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/WattTimeClientConfigurationTests.cs @@ -26,8 +26,7 @@ public void Validate_DoesNotThrow(string? username, string? password, string? ur [TestCase("testuser", "12345", "not a url", TestName = "Validate throws: username; password; bad url")] [TestCase(null, "12345", "http://example.com", TestName = "Validate throws: no username; password; url")] - [TestCase("testuser", null, "http://example.com", TestName = "Validate throws: no username; password; url")] - [TestCase(null, "password", "http://example.com", TestName = "Validate throws: username; no password; url")] + [TestCase("testuser", null, "http://example.com", TestName = "Validate throws: username; no password; url")] public void Validate_Throws(string? username, string? password, string? url) { diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs index 9d721cf56..3cadb1648 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs @@ -1,14 +1,18 @@ -using CarbonAware.DataSources.WattTime.Client; +using CarbonAware.Configuration; +using CarbonAware.DataSources.WattTime.Client; using CarbonAware.DataSources.WattTime.Model; using CarbonAware.Exceptions; using CarbonAware.Interfaces; using CarbonAware.Model; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; namespace CarbonAware.DataSources.WattTime.Tests; @@ -35,6 +39,14 @@ class WattTimeDataSourceTests private const double GRAMS_TO_POUNDS = 0.00220462262185; private const double KWH_TO_MWH = 0.001; + private readonly string TypeKey = $"Configurations:WattTime:Type"; + private readonly string UsernameKey = $"Configurations:WattTime:Username"; + private readonly string PasswordKey = $"Configurations:WattTime:Password"; + private readonly string UseProxyKey = $"Configurations:WattTime:Proxy:UseProxy"; + private readonly string ProxyUrlKey = $"Configurations:WattTime:Proxy:Url"; + private readonly string ProxyUsernameKey = $"Configurations:WattTime:Proxy:Username"; + private readonly string ProxyPasswordKey = $"Configurations:WattTime:Proxy:Password"; + [SetUp] public void Setup() @@ -288,6 +300,81 @@ public async Task GetCarbonIntensity_CalculatesDurationBasedOnFrequency(double[] CollectionAssert.AreEqual(expectedDurationList, actualDurationList); } + [Test] + public void ConfigureDI_ClientProxyTest_With_Missing_ProxyURL_ThrowsException() + { + // Arrange + var inMemorySettings = new Dictionary { + { TypeKey, "WattTime" }, + { UsernameKey, "testUsername" }, + { PasswordKey, "testPassword123!" }, + { UseProxyKey, "true" }, + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + var dataSourceConfig = new DataSourcesConfiguration() + { + EmissionsDataSource = "WattTime", + ForecastDataSource = "WattTime", + Section = configuration.GetSection("Configurations") + }; + + var serviceCollection = new ServiceCollection(); + + // Act & Assert + Assert.Throws(() => WattTimeDataSource.ConfigureDI(serviceCollection, dataSourceConfig)); + Assert.Throws(() => WattTimeDataSource.ConfigureDI(serviceCollection, dataSourceConfig)); + } + + [TestCase(true, TestName = "ClientProxyTest, successful: denotes adding WattTime data sources using proxy.")] + [TestCase(false, TestName = "ClientProxyTest, successful: denotes adding WattTime data sources without using proxy.")] + public void ConfigureDI_ClientProxyTest_AddsDataSource(bool withProxyUrl) + { + // Arrange + var inMemorySettings = new Dictionary { + { TypeKey, "WattTime" }, + { UsernameKey, "testUsername" }, + { PasswordKey, "testPassword123!" }, + { UseProxyKey, withProxyUrl.ToString() }, + }; + + if (withProxyUrl) + { + inMemorySettings.Add(ProxyUrlKey, "http://10.10.10.1"); + inMemorySettings.Add(ProxyUsernameKey, "proxyUsername"); + inMemorySettings.Add(ProxyPasswordKey, "proxyPassword"); + } + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + var dataSourceConfig = new DataSourcesConfiguration() + { + EmissionsDataSource = "WattTime", + ForecastDataSource = "WattTime", + Section = configuration.GetSection("Configurations") + }; + + var serviceCollection = new ServiceCollection(); + var emissionsDescriptor = new ServiceDescriptor(typeof(IEmissionsDataSource), typeof(WattTimeDataSource), ServiceLifetime.Singleton); + var forecastDescriptor = new ServiceDescriptor(typeof(IForecastDataSource), typeof(WattTimeDataSource), ServiceLifetime.Singleton); + + Assert.That(!serviceCollection.Any( i => i.ToString() == emissionsDescriptor.ToString())); + Assert.That(!serviceCollection.Any( i => i.ToString() == forecastDescriptor.ToString())); + + // Act + WattTimeDataSource.ConfigureDI(serviceCollection, dataSourceConfig); + WattTimeDataSource.ConfigureDI(serviceCollection, dataSourceConfig); + + // Assert + Assert.That(serviceCollection.Any( i => i.ToString() == emissionsDescriptor.ToString())); + Assert.That(serviceCollection.Any( i => i.ToString() == forecastDescriptor.ToString())); + } + private void MockBalancingAuthorityLocationMapping() { this.LocationSource.Setup(r => r.ToGeopositionLocationAsync(this.DefaultLocation)).Returns(Task.FromResult(this.DefaultLocation)); diff --git a/src/CarbonAware.Tools/CarbonAware.Tools.AzureRegionTestDataGenerator/Properties/launchSettings.json b/src/CarbonAware.Tools/CarbonAware.Tools.AzureRegionTestDataGenerator/Properties/launchSettings.json new file mode 100644 index 000000000..33504c948 --- /dev/null +++ b/src/CarbonAware.Tools/CarbonAware.Tools.AzureRegionTestDataGenerator/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "WSL": { + "commandName": "WSL2", + "distributionName": "" + } + } +} \ No newline at end of file diff --git a/src/CarbonAware.WebApi/src/Program.cs b/src/CarbonAware.WebApi/src/Program.cs index 9a70c15ab..995ea034b 100644 --- a/src/CarbonAware.WebApi/src/Program.cs +++ b/src/CarbonAware.WebApi/src/Program.cs @@ -1,9 +1,9 @@ using CarbonAware; using CarbonAware.WebApi.Configuration; -using Microsoft.AspNetCore.StaticFiles; using CarbonAware.WebApi.Filters; using GSF.CarbonAware.Configuration; using GSF.CarbonAware.Exceptions; +using Microsoft.AspNetCore.StaticFiles; using Microsoft.OpenApi.Models; using OpenTelemetry.Resources; using OpenTelemetry.Trace; diff --git a/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj b/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj index df1ed5ada..8cad60d2e 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj +++ b/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj @@ -28,9 +28,9 @@ Include="..\..\..\CarbonAware.DataSources\CarbonAware.DataSources.WattTime\mock\CarbonAware.DataSources.WattTime.Mocks.csproj" /> - + Include="..\..\..\CarbonAware.DataSources\CarbonAware.DataSources.ElectricityMapsFree\mock\CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj" /> + diff --git a/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs b/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs index 700c3ffc8..9da43e4fc 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs +++ b/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs @@ -1,4 +1,3 @@ -using CarbonAware.DataSources.Configuration; using CarbonAware.WebApi.IntegrationTests; using CarbonAware.WebApi.Models; using NUnit.Framework; @@ -69,8 +68,8 @@ public async Task BestLocations_ReturnsOK(DateTimeOffset start, DateTimeOffset e Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); } - [TestCase("location", "", TestName = "empty location query string")] - [TestCase("non-location-param", "", TestName = "location param not present")] + [TestCase("location", "", TestName = "BestLocations_EmptyLocationQueryString: empty location query string")] + [TestCase("non-location-param", "", TestName = "BestLocations_EmptyLocationQueryString: location param not present")] public async Task BestLocations_EmptyLocationQueryString_ReturnsBadRequest(string queryString, string value) { //Call the private method to construct with parameters @@ -107,7 +106,7 @@ public async Task EmissionsForecastsCurrent_SupportedDataSources_ReturnsOk() [Test] public async Task EmissionsForecastsCurrent_StartAndEndOutsideWindow_ReturnsBadRequest() { - IgnoreTestForDataSource("data source does not implement '/emissions/forecasts/current'", DataSourceType.JSON); + IgnoreTestForDataSource($"data source does not implement '{currentForecastURI}'", DataSourceType.JSON, DataSourceType.ElectricityMapsFree); _dataSourceMocker?.SetupForecastMock(); @@ -130,7 +129,7 @@ public async Task EmissionsForecastsCurrent_StartAndEndOutsideWindow_ReturnsBadR public async Task EmissionsForecastsCurrent_InvalidLocationQueryString_ReturnsBadRequest(string queryString, string value) { - IgnoreTestForDataSource("data source does not implement '/emissions/forecasts/current'", DataSourceType.JSON); + IgnoreTestForDataSource($"data source does not implement '{currentForecastURI}'", DataSourceType.JSON, DataSourceType.ElectricityMapsFree); _dataSourceMocker?.SetupForecastMock(); @@ -150,7 +149,7 @@ public async Task EmissionsForecastsCurrent_InvalidLocationQueryString_ReturnsBa [TestCase("eastus", "2021-9-1T08:30:00Z", TestName = "EmissionsForecastsBatch returns BadRequest for wrong date format")] public async Task EmissionsForecastsBatch_MissingRequiredParams_ReturnsBadRequest(string location, string requestedAt) { - IgnoreTestForDataSource("data source does not implement '/emissions/forecasts/batch'", DataSourceType.JSON); + IgnoreTestForDataSource($"data source does not implement '{batchForecastURI}'", DataSourceType.JSON, DataSourceType.ElectricityMapsFree); _dataSourceMocker?.SetupForecastMock(); var forecastData = Enumerable.Range(0, 1).Select(x => new @@ -242,6 +241,8 @@ public async Task EmissionsMarginalCarbonIntensity_ReturnsOk(string start, strin [TestCase("non-location-param", "", TestName = "EmissionsMarginalCarbonIntensity returns BadRequest for location not present")] public async Task EmissionsMarginalCarbonIntensity_EmptyLocationQueryString_ReturnsBadRequest(string queryString, string value) { + IgnoreTestForDataSource($"data source does not implement '{averageCarbonIntensityURI}'", DataSourceType.ElectricityMapsFree); + var queryStrings = new Dictionary(); queryStrings[queryString] = value; @@ -259,6 +260,8 @@ public async Task EmissionsMarginalCarbonIntensity_EmptyLocationQueryString_Retu [TestCase("westus", "2022-3-1T15:30:00Z", "2022-3-1T18:00:00Z", TestName = "EmissionsMarginalCarbonIntensityBatch returns BadRequest for wrong date format")] public async Task EmissionsMarginalCarbonIntensityBatch_MissingRequiredParams_ReturnsBadRequest(string location, string startTime, string endTime) { + IgnoreTestForDataSource($"data source does not implement '{batchAverageCarbonIntensityURI}'", DataSourceType.ElectricityMapsFree); + var intesityData = Enumerable.Range(0, 1).Select(x => new { location = location, diff --git a/src/CarbonAware.WebApi/test/integrationTests/IntegrationTestingBase.cs b/src/CarbonAware.WebApi/test/integrationTests/IntegrationTestingBase.cs index 5dcb499bd..41771fefd 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/IntegrationTestingBase.cs +++ b/src/CarbonAware.WebApi/test/integrationTests/IntegrationTestingBase.cs @@ -1,7 +1,7 @@ -using CarbonAware.DataSources.Configuration; -using CarbonAware.Interfaces; +using CarbonAware.Interfaces; using CarbonAware.DataSources.Json.Mocks; using CarbonAware.DataSources.ElectricityMaps.Mocks; +using CarbonAware.DataSources.ElectricityMapsFree.Mocks; using CarbonAware.DataSources.WattTime.Mocks; using Microsoft.AspNetCore.Mvc.Testing; using NUnit.Framework; @@ -12,6 +12,15 @@ namespace CarbonAware.WebApi.IntegrationTests; +public enum DataSourceType +{ + None, + WattTime, + JSON, + ElectricityMaps, + ElectricityMapsFree, +} + /// /// A base class that does all the common setup for the Integration Testing /// Overrides WebAPI factory by switching out different configurations via _datasource @@ -112,6 +121,16 @@ public void Setup() _dataSourceMocker = new ElectricityMapsDataSourceMocker(); break; } + case DataSourceType.ElectricityMapsFree: + { + Environment.SetEnvironmentVariable("DataSources__EmissionsDataSource", "ElectricityMapsFree"); + Environment.SetEnvironmentVariable("DataSources__ForecastDataSource", ""); + Environment.SetEnvironmentVariable("DataSources__Configurations__ElectricityMapsFree__Type", "ElectricityMapsFree"); + Environment.SetEnvironmentVariable("DataSources__Configurations__ElectricityMapsFree__Token", "test"); + + _dataSourceMocker = new ElectricityMapsFreeDataSourceMocker(); + break; + } case DataSourceType.None: { Environment.SetEnvironmentVariable("DataSources__EmissionsDataSource", ""); diff --git a/src/CarbonAware.WebApi/test/integrationTests/LocationsControllerTests.cs b/src/CarbonAware.WebApi/test/integrationTests/LocationsControllerTests.cs index 1a1fd79c1..1049abd88 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/LocationsControllerTests.cs +++ b/src/CarbonAware.WebApi/test/integrationTests/LocationsControllerTests.cs @@ -1,4 +1,3 @@ -using CarbonAware.DataSources.Configuration; using CarbonAware.WebApi.IntegrationTests; using NUnit.Framework; using System.Net; diff --git a/src/CarbonAware.WebApi/test/integrationTests/UnconfiguredWebApiTests.cs b/src/CarbonAware.WebApi/test/integrationTests/UnconfiguredWebApiTests.cs deleted file mode 100644 index 665b3884a..000000000 --- a/src/CarbonAware.WebApi/test/integrationTests/UnconfiguredWebApiTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using CarbonAware.DataSources.Configuration; -using NUnit.Framework; -using System.Net; - -namespace CarbonAware.WebApi.IntegrationTests; - -/// -/// Tests that the Web API starts without configuration. -/// -[TestFixture(DataSourceType.None)] -class UnconfiguredWebApiTests : IntegrationTestingBase -{ - private readonly string healthURI = "/health"; - private readonly string fakeURI = "/fake-endpoint"; - - public UnconfiguredWebApiTests(DataSourceType dataSource) : base(dataSource) { } - - [Test] - public async Task HealthCheck_ReturnsOK() - { - //Use client to get endpoint - var result = await _client.GetAsync(healthURI); - Assert.That(result, Is.Not.Null); - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); - } - - [Test] - public async Task FakeEndPoint_ReturnsNotFound() - { - var result = await _client.GetAsync(fakeURI); - Assert.That(result, Is.Not.Null); - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); - } -} \ No newline at end of file diff --git a/src/CarbonAware/src/CarbonAware.csproj b/src/CarbonAware/src/CarbonAware.csproj index d691e0f80..3e8ec44e4 100644 --- a/src/CarbonAware/src/CarbonAware.csproj +++ b/src/CarbonAware/src/CarbonAware.csproj @@ -31,7 +31,6 @@ - diff --git a/src/CarbonAware/src/Configuration/DataSourcesConfiguration.cs b/src/CarbonAware/src/Configuration/DataSourcesConfiguration.cs index 7b54c9c51..8b738ce1a 100644 --- a/src/CarbonAware/src/Configuration/DataSourcesConfiguration.cs +++ b/src/CarbonAware/src/Configuration/DataSourcesConfiguration.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Configuration; +using CarbonAware.Interfaces; +using Microsoft.Extensions.Configuration; namespace CarbonAware.Configuration; internal class DataSourcesConfiguration @@ -8,9 +9,29 @@ internal class DataSourcesConfiguration #nullable enable public string? EmissionsDataSource { get; set; } public string? ForecastDataSource { get; set; } - public IConfigurationSection? ConfigurationSection { get; set; } + public IConfigurationSection? Section { get; set; } #nullable disable + /// + /// Gets the "Type" field for configuration object associated with a setting by data source interface type. + /// + /// Returns the string value of the associated "Type" field. + public string ConfigurationType() where T : IDataSource + { + if (typeof(T) == typeof(IEmissionsDataSource)) + { + return EmissionsConfigurationType(); + } + else if (typeof(T) == typeof(IForecastDataSource)) + { + return ForecastConfigurationType(); + } + else + { + throw new ArgumentException($"Data source interface type '{typeof(T)}' is not supported."); + } + } + /// /// Gets the "Type" field for configuration object associated with EmissionsDataSource setting. /// @@ -29,6 +50,26 @@ public string ForecastConfigurationType() return GetConfigurationType(ForecastDataSource); } + /// + /// Gets the entire configuration object associated with a setting by data source interface type. + /// + /// Returns the of the associated data source interface. + public IConfigurationSection ConfigurationSection() where T : IDataSource + { + if (typeof(T) == typeof(IEmissionsDataSource)) + { + return EmissionsConfigurationSection(); + } + else if (typeof(T) == typeof(IForecastDataSource)) + { + return ForecastConfigurationSection(); + } + else + { + throw new ArgumentException($"Data source interface type '{typeof(T)}' is not supported."); + } + } + /// /// Gets the entire configuration object associated with EmissionsDataSource setting. /// @@ -66,17 +107,17 @@ public void AssertValid() private string GetConfigurationType(string dataSourceName) { - return ConfigurationSection.GetValue($"{dataSourceName}:Type"); + return Section.GetValue($"{dataSourceName}:Type"); } private IConfigurationSection GetConfigurationSection(string dataSourceName) { - return ConfigurationSection.GetSection(dataSourceName); + return Section.GetSection(dataSourceName); } private bool ConfigurationSectionContainsKey(string key) { - foreach (var subsection in ConfigurationSection.GetChildren()) + foreach (var subsection in Section.GetChildren()) { if (subsection.Key == key) { diff --git a/src/CarbonAware/src/Configuration/DataSourcesConfigurationExtensions.cs b/src/CarbonAware/src/Configuration/DataSourcesConfigurationExtensions.cs index 6894b14c4..30aa90539 100644 --- a/src/CarbonAware/src/Configuration/DataSourcesConfigurationExtensions.cs +++ b/src/CarbonAware/src/Configuration/DataSourcesConfigurationExtensions.cs @@ -7,7 +7,7 @@ internal static class DataSourcesConfigurationExtensions public static DataSourcesConfiguration DataSources(this IConfiguration configuration) { var dataSources = configuration.GetSection(DataSourcesConfiguration.Key).Get() ?? new DataSourcesConfiguration(); - dataSources.ConfigurationSection = configuration.GetSection($"{DataSourcesConfiguration.Key}:Configurations"); + dataSources.Section = configuration.GetSection($"{DataSourcesConfiguration.Key}:Configurations"); dataSources.AssertValid(); return dataSources; diff --git a/src/CarbonAware/src/Extensions/ServiceCollectionExtensions.cs b/src/CarbonAware/src/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..8a1ecb486 --- /dev/null +++ b/src/CarbonAware/src/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,80 @@ +using CarbonAware.Configuration; +using CarbonAware.Exceptions; +using CarbonAware.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Reflection; + +namespace CarbonAware.Extensions; +internal static class ServiceCollectionExtensions +{ + public static IServiceCollection AddDataSourceServices(this IServiceCollection services, IConfiguration configuration) + { + var noEmissionsDataSource = false; + var noForecastDataSource = false; + + try + { + services.AddDataSource(configuration); + } + catch (Exception) + { + services.TryAddSingleton(); + noEmissionsDataSource = true; + } + + try + { + services.AddDataSource(configuration); + } + catch (Exception) + { + services.TryAddSingleton(); + noForecastDataSource = true; + } + + if (noEmissionsDataSource && noForecastDataSource) + { + throw new ConfigurationException("No data sources are configured"); + } + + return services; + } + + public static IServiceCollection AddDataSource(this IServiceCollection services, IConfiguration configuration) + where T : IDataSource + { + // Get the config + var dataSources = configuration.DataSources(); + + // Load the assembly for the configured IDataSource interface, T. + Assembly assembly; + try + { + assembly = Assembly.Load($"CarbonAware.DataSources.{dataSources.ConfigurationType()}"); + } catch (Exception e) + { + throw new ConfigurationException($"Could not load assembly for data source '{dataSources.ConfigurationType()}'", e); + } + + + // Get the classes that implement the interface, T. + // Pick the first, because we only expect one per interface. + Type dataSourceType = assembly.GetTypes() + .Where(type => typeof(T).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract) + .First() ?? throw new ConfigurationException("No data sources are configured"); + + // Call static configuration method on the data source to allow it + // to configure itself and its dependencies. + MethodInfo configureMethod = dataSourceType.GetMethod( + "ConfigureDI", + BindingFlags.Static + | BindingFlags.Public) + ?.MakeGenericMethod(typeof(T)) ?? throw new ConfigurationException("No data method 'ConfigureDI' is configured"); + + configureMethod.Invoke(null, new Object[2] {services, dataSources}); + + return services; + } +} diff --git a/src/CarbonAware/src/Interfaces/IDataSource.cs b/src/CarbonAware/src/Interfaces/IDataSource.cs new file mode 100644 index 000000000..98e1d3e3d --- /dev/null +++ b/src/CarbonAware/src/Interfaces/IDataSource.cs @@ -0,0 +1,12 @@ +using CarbonAware.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace CarbonAware.Interfaces; + +internal interface IDataSource +{ + public static IServiceCollection ConfigureDI(IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/CarbonAware/src/Interfaces/IEmissionsDataSource.cs b/src/CarbonAware/src/Interfaces/IEmissionsDataSource.cs index fa9351a40..56808ce13 100644 --- a/src/CarbonAware/src/Interfaces/IEmissionsDataSource.cs +++ b/src/CarbonAware/src/Interfaces/IEmissionsDataSource.cs @@ -1,6 +1,6 @@ namespace CarbonAware.Interfaces; -internal interface IEmissionsDataSource +internal interface IEmissionsDataSource : IDataSource { /// /// Gets the carbon intensity for multiple locations for a given start and end time diff --git a/src/CarbonAware/src/Interfaces/IForecastDataSource.cs b/src/CarbonAware/src/Interfaces/IForecastDataSource.cs index cd8d06c08..cbfc0b8fa 100644 --- a/src/CarbonAware/src/Interfaces/IForecastDataSource.cs +++ b/src/CarbonAware/src/Interfaces/IForecastDataSource.cs @@ -1,6 +1,6 @@ namespace CarbonAware.Interfaces; -internal interface IForecastDataSource +internal interface IForecastDataSource : IDataSource { /// /// Gets the current forecasted carbon intensity for a location diff --git a/src/CarbonAware/test/Configuration/DataSourcesConfigurationTests.cs b/src/CarbonAware/test/Configuration/DataSourcesConfigurationTests.cs index 671435bbb..26787f6c2 100644 --- a/src/CarbonAware/test/Configuration/DataSourcesConfigurationTests.cs +++ b/src/CarbonAware/test/Configuration/DataSourcesConfigurationTests.cs @@ -27,7 +27,7 @@ public void AssertValid_WithAtleastOneDataSourceConfigured_DoesNotThrowException { EmissionsDataSource = emissionsDataSource, ForecastDataSource = forecastDataSource, - ConfigurationSection = configuration.GetSection("Configurations") + Section = configuration.GetSection("Configurations") }; Assert.DoesNotThrow(() => dataSourceConfig.AssertValid()); @@ -51,7 +51,7 @@ public void AssertValid_DataSourceNotInConfigurationSection_ThrowsException(stri { EmissionsDataSource = emissionsDataSource, ForecastDataSource = forecastDataSource, - ConfigurationSection = configuration.GetSection("Configurations") + Section = configuration.GetSection("Configurations") }; var ex = Assert.Throws(() => dataSourceConfig.AssertValid()); Assert.That(ex!.Message, Contains.Substring(errorMessage)); @@ -78,7 +78,7 @@ public void ConfigurationTypesAndSections_ReturnCorrectType(string forecastDataS { EmissionsDataSource = emissionsDataSource, ForecastDataSource = forecastDataSource, - ConfigurationSection = configuration.GetSection("Configurations") + Section = configuration.GetSection("Configurations") }; // Act diff --git a/src/CarbonAwareSDK.sln b/src/CarbonAwareSDK.sln index 82672374a..06256aa9f 100644 --- a/src/CarbonAwareSDK.sln +++ b/src/CarbonAwareSDK.sln @@ -25,8 +25,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarbonAware.DataSources.Wat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarbonAware.DataSources.WattTime", "CarbonAware.DataSources\CarbonAware.DataSources.WattTime\src\CarbonAware.DataSources.WattTime.csproj", "{12D19F2D-0427-4591-949D-0DCDEDACF5B2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarbonAware.DataSources.Registration", "CarbonAware.DataSources\CarbonAware.DataSources.Registration\CarbonAware.DataSources.Registration.csproj", "{5CFC4F4D-7E8D-4682-A659-35050C637FF4}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarbonAware.LocationSources", "CarbonAware.LocationSources\src\CarbonAware.LocationSources.csproj", "{C120DB7C-E4D8-4A8F-B180-0C5D0D8C085C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarbonAware.LocationSources.Test", "CarbonAware.LocationSources\test\CarbonAware.LocationSources.Test.csproj", "{F4BD1233-447F-4245-AE7B-A877427EB2D7}" @@ -107,10 +105,6 @@ Global {12D19F2D-0427-4591-949D-0DCDEDACF5B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {12D19F2D-0427-4591-949D-0DCDEDACF5B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {12D19F2D-0427-4591-949D-0DCDEDACF5B2}.Release|Any CPU.Build.0 = Release|Any CPU - {5CFC4F4D-7E8D-4682-A659-35050C637FF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5CFC4F4D-7E8D-4682-A659-35050C637FF4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5CFC4F4D-7E8D-4682-A659-35050C637FF4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5CFC4F4D-7E8D-4682-A659-35050C637FF4}.Release|Any CPU.Build.0 = Release|Any CPU {C120DB7C-E4D8-4A8F-B180-0C5D0D8C085C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C120DB7C-E4D8-4A8F-B180-0C5D0D8C085C}.Debug|Any CPU.Build.0 = Debug|Any CPU {C120DB7C-E4D8-4A8F-B180-0C5D0D8C085C}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs b/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs index 459351ae6..5be9ebc0a 100644 --- a/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs +++ b/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs @@ -1,5 +1,5 @@ -using CarbonAware.DataSources.Configuration; using CarbonAware.Interfaces; +using CarbonAware.Extensions; using CarbonAware.LocationSources; using CarbonAware.LocationSources.Configuration; using GSF.CarbonAware.Handlers; @@ -21,7 +21,7 @@ public static IServiceCollection AddEmissionsServices(this IServiceCollection se configuration.GetSection(LocationDataSourcesConfiguration.Key).Bind(c); }); services.TryAddSingleton(); - services.AddDataSourceService(configuration); + services.AddDataSourceServices(configuration); services.TryAddSingleton(); services.TryAddSingleton(); return services; @@ -37,7 +37,7 @@ public static IServiceCollection AddForecastServices(this IServiceCollection ser configuration.GetSection(LocationDataSourcesConfiguration.Key).Bind(c); }); services.TryAddSingleton(); - services.AddDataSourceService(configuration); + services.AddDataSourceServices(configuration); services.TryAddSingleton(); services.TryAddSingleton(); return services; diff --git a/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj b/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj index f113e0b98..d6bd80083 100644 --- a/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj +++ b/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj @@ -18,12 +18,17 @@ - + + + + diff --git a/src/GSF.CarbonAware/test/Configuration/ServiceCollectionExtensionsTests.cs b/src/GSF.CarbonAware/test/Configuration/ServiceCollectionExtensionsTests.cs index a129bfbde..72026ff86 100644 --- a/src/GSF.CarbonAware/test/Configuration/ServiceCollectionExtensionsTests.cs +++ b/src/GSF.CarbonAware/test/Configuration/ServiceCollectionExtensionsTests.cs @@ -23,7 +23,7 @@ public void AddEmissionsServices_ReturnsServices() { "LocationDataSourcesConfiguration:LocationSourceFiles:Delimiter", "-" }, { "DataSources:EmissionsDataSource", "Json" }, { "DataSources:ForecastDataSource", "" }, - { "DataSources:Configurations:Json:Type", "JSON" }, + { "DataSources:Configurations:Json:Type", "Json" }, { "DataSources:Configurations:Json:DataFileLocation", "test-data-azure-emissions.json" } }; var configuration = new ConfigurationBuilder() diff --git a/src/GSF.CarbonAware/test/Handlers/EmissionsHandlerTests.cs b/src/GSF.CarbonAware/test/Handlers/EmissionsHandlerTests.cs index 1ee70763a..c628604c3 100644 --- a/src/GSF.CarbonAware/test/Handlers/EmissionsHandlerTests.cs +++ b/src/GSF.CarbonAware/test/Handlers/EmissionsHandlerTests.cs @@ -160,8 +160,8 @@ public async Task GetBestEmissionsDataAsync_ReturnsExpected() } [TestCase("2000-01-01T00:00:00Z", "2000-01-02T00:00:00Z", 0, TestName = "GetBestEmissionsDataAsync calls data source with expected dates: start & end")] - [TestCase("2000-01-01T00:00:00Z", null, 1000, TestName = "GetBestEmissionsDataAsync calls data source with expected dates: only start")] - [TestCase(null, null, 1000, TestName = "GetBestEmissionsDataAsync calls data source with expected dates: no start or end")] + [TestCase("2000-01-01T00:00:00Z", null, 10000, TestName = "GetBestEmissionsDataAsync calls data source with expected dates: only start")] + [TestCase(null, null, 10000, TestName = "GetBestEmissionsDataAsync calls data source with expected dates: no start or end")] public async Task GetBestEmissionsDataAsync_CallsWithExpectedDates(DateTimeOffset? start, DateTimeOffset? end, long tickTolerance) { // Arrange @@ -203,7 +203,7 @@ public async Task GetBestEmissionsDataAsync_CallsWithExpectedDates(DateTimeOffse // Because this method uses DateTimeOffset.UtcNow as a default, we cannot precisely check our date expectations // so instead, we test that the dates are within a tolerable range using DateTimeOffset Ticks. // 1 Second == 10,000,000 Ticks - // 1000 Ticks == 0.1 Milliseconds + // 10,000 Ticks == 1 Millisecond var minAllowableStartTicks = expectedStart.Ticks - tickTolerance; var maxAllowableStartTicks = expectedStart.Ticks + tickTolerance; var minAllowableEndTicks = expectedEnd.Ticks - tickTolerance;