From f7ed0e4de83dc7bf74b41d13214bcbda90c7895c Mon Sep 17 00:00:00 2001 From: romankr Date: Sun, 26 Nov 2023 17:03:51 +0400 Subject: [PATCH] next batch of unit tests --- .../Configuration/LeaguesFactoryTests.cs | 53 ----------------- .../Configuration/OddsApiOptionsTests.cs | 57 +++++++++++++++++++ .../OddsApi/Configuration/LeaguesFactory.cs | 14 ----- .../OddsApi/Configuration/OddsApiOptions.cs | 10 ++++ .../Program.cs | 2 +- .../GlobalUsings.cs | 0 ...lector.Functions.Notification.Tests.csproj | 28 +++++++++ .../Configuration/EmailSenderOptions.cs | 42 +++++++++++++- .../CommunicationServices/EmailSender.cs | 2 +- .../CosmosDb/Configuration/CosmosDbOptions.cs | 12 ---- .../CosmosDb/ContainerFactory.cs | 26 +++++++++ .../CosmosDb/CosmosDbClient.cs | 7 +-- .../NotificationFunction.cs | 7 ++- .../Program.cs | 25 ++++---- .../Program.cs | 2 +- OddsCollector.sln | 12 +++- 16 files changed, 194 insertions(+), 105 deletions(-) delete mode 100644 OddsCollector.Common.Tests/OddsApi/Configuration/LeaguesFactoryTests.cs create mode 100644 OddsCollector.Common.Tests/OddsApi/Configuration/OddsApiOptionsTests.cs delete mode 100644 OddsCollector.Common/OddsApi/Configuration/LeaguesFactory.cs create mode 100644 OddsCollector.Functions.Notification.Tests/GlobalUsings.cs create mode 100644 OddsCollector.Functions.Notification.Tests/OddsCollector.Functions.Notification.Tests.csproj delete mode 100644 OddsCollector.Functions.Notification/CosmosDb/Configuration/CosmosDbOptions.cs create mode 100644 OddsCollector.Functions.Notification/CosmosDb/ContainerFactory.cs diff --git a/OddsCollector.Common.Tests/OddsApi/Configuration/LeaguesFactoryTests.cs b/OddsCollector.Common.Tests/OddsApi/Configuration/LeaguesFactoryTests.cs deleted file mode 100644 index 4c04f41..0000000 --- a/OddsCollector.Common.Tests/OddsApi/Configuration/LeaguesFactoryTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using FluentAssertions; -using OddsCollector.Common.OddsApi.Configuration; - -namespace OddsCollector.Common.Tests.OddsApi.Configuration; - -internal sealed class LeaguesFactoryTests -{ - [Test] - public void CreateOddsApiOptions_WithValidLeague_ReturnsNewInstance() - { - string league = nameof(league); - - var leagues = LeaguesFactory.CreateLeagues(league); - - leagues.Should().NotBeNull().And.HaveCount(1); - leagues.ElementAt(0).Should().NotBeNull().And.Be(league); - } - - [Test] - public void CreateOddsApiOptions_WithValidLeagues_ReturnsNewInstance() - { - var leagues = "league1;league2"; - - var result = LeaguesFactory.CreateLeagues(leagues); - - result.Should().NotBeNull().And.HaveCount(2); - result.ElementAt(0).Should().NotBeNull().And.Be("league1"); - result.ElementAt(1).Should().NotBeNull().And.Be("league2"); - } - - [Test] - public void CreateOddsApiOptions_WithDuplicatedLeagues_ReturnsNewInstance() - { - var leagues = "league1;league1"; - - var result = LeaguesFactory.CreateLeagues(leagues); - - result.Should().NotBeNull().And.HaveCount(1); - result.ElementAt(0).Should().NotBeNull().And.Be("league1"); - } - - [TestCase("")] - [TestCase(null)] - public void CreateOddsApiOptions_WithNullOrEmptyLeagues_ThrowsException(string? leagues) - { - var action = () => - { - _ = LeaguesFactory.CreateLeagues(leagues); - }; - - action.Should().Throw().WithParameterName(nameof(leagues)); - } -} diff --git a/OddsCollector.Common.Tests/OddsApi/Configuration/OddsApiOptionsTests.cs b/OddsCollector.Common.Tests/OddsApi/Configuration/OddsApiOptionsTests.cs new file mode 100644 index 0000000..f0a7af1 --- /dev/null +++ b/OddsCollector.Common.Tests/OddsApi/Configuration/OddsApiOptionsTests.cs @@ -0,0 +1,57 @@ +using FluentAssertions; +using OddsCollector.Common.OddsApi.Configuration; + +namespace OddsCollector.Common.Tests.OddsApi.Configuration; + +internal sealed class OddsApiOptionsTests +{ + [Test] + public void SetLeagues_WithValidLeague_ReturnsValidInstance() + { + string league = nameof(league); + + var options = new OddsApiOptions(); + options.SetLeagues(league); + + options.Leagues.Should().NotBeNull().And.HaveCount(1); + options.Leagues.ElementAt(0).Should().NotBeNull().And.Be(league); + } + + [Test] + public void SetLeagues_WithValidLeagues_ReturnsValidInstance() + { + var leagues = "league1;league2"; + + var options = new OddsApiOptions(); + options.SetLeagues(leagues); + + options.Leagues.Should().NotBeNull().And.HaveCount(2); + options.Leagues.ElementAt(0).Should().NotBeNull().And.Be("league1"); + options.Leagues.ElementAt(1).Should().NotBeNull().And.Be("league2"); + } + + [Test] + public void SetLeagues_WithDuplicatedLeagues_ReturnsNewInstance() + { + var leagues = "league1;league1"; + + var options = new OddsApiOptions(); + options.SetLeagues(leagues); + + options.Leagues.Should().NotBeNull().And.HaveCount(1); + options.Leagues.ElementAt(0).Should().NotBeNull().And.Be("league1"); + } + + [TestCase("")] + [TestCase(null)] + public void SetLeagues_WithNullOrEmptyLeagues_ThrowsException(string? leagues) + { + var action = () => + { + var options = new OddsApiOptions(); + options.SetLeagues(leagues); + }; + + action.Should().Throw().WithParameterName(nameof(leagues)); + } +} diff --git a/OddsCollector.Common/OddsApi/Configuration/LeaguesFactory.cs b/OddsCollector.Common/OddsApi/Configuration/LeaguesFactory.cs deleted file mode 100644 index d01d61f..0000000 --- a/OddsCollector.Common/OddsApi/Configuration/LeaguesFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace OddsCollector.Common.OddsApi.Configuration; - -public static class LeaguesFactory -{ - public static HashSet CreateLeagues(string? leagues) - { - if (string.IsNullOrEmpty(leagues)) - { - throw new ArgumentException($"{nameof(leagues)} cannot be null or empty", nameof(leagues)); - } - - return leagues.Split(";").ToHashSet(); - } -} diff --git a/OddsCollector.Common/OddsApi/Configuration/OddsApiOptions.cs b/OddsCollector.Common/OddsApi/Configuration/OddsApiOptions.cs index 053ad31..73ad041 100644 --- a/OddsCollector.Common/OddsApi/Configuration/OddsApiOptions.cs +++ b/OddsCollector.Common/OddsApi/Configuration/OddsApiOptions.cs @@ -8,4 +8,14 @@ public class OddsApiOptions [Required] [SuppressMessage("Usage", "CA2227:Collection properties should be read only")] public HashSet Leagues { get; set; } = new(); + + public void SetLeagues(string? leagues) + { + if (string.IsNullOrEmpty(leagues)) + { + throw new ArgumentException($"{nameof(leagues)} cannot be null or empty", nameof(leagues)); + } + + Leagues = leagues.Split(";").ToHashSet(); + } } diff --git a/OddsCollector.Functions.EventResults/Program.cs b/OddsCollector.Functions.EventResults/Program.cs index 1602a5c..31fd818 100644 --- a/OddsCollector.Functions.EventResults/Program.cs +++ b/OddsCollector.Functions.EventResults/Program.cs @@ -15,7 +15,7 @@ services.Configure(o => { // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 - o.Leagues = LeaguesFactory.CreateLeagues(Environment.GetEnvironmentVariable("OddsApi:Leagues")); + o.SetLeagues(Environment.GetEnvironmentVariable("OddsApi:Leagues")); }); services.AddHttpClient(); services.AddSingleton(); diff --git a/OddsCollector.Functions.Notification.Tests/GlobalUsings.cs b/OddsCollector.Functions.Notification.Tests/GlobalUsings.cs new file mode 100644 index 0000000..e69de29 diff --git a/OddsCollector.Functions.Notification.Tests/OddsCollector.Functions.Notification.Tests.csproj b/OddsCollector.Functions.Notification.Tests/OddsCollector.Functions.Notification.Tests.csproj new file mode 100644 index 0000000..e15bfe5 --- /dev/null +++ b/OddsCollector.Functions.Notification.Tests/OddsCollector.Functions.Notification.Tests.csproj @@ -0,0 +1,28 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/OddsCollector.Functions.Notification/CommunicationServices/Configuration/EmailSenderOptions.cs b/OddsCollector.Functions.Notification/CommunicationServices/Configuration/EmailSenderOptions.cs index c637e62..c569033 100644 --- a/OddsCollector.Functions.Notification/CommunicationServices/Configuration/EmailSenderOptions.cs +++ b/OddsCollector.Functions.Notification/CommunicationServices/Configuration/EmailSenderOptions.cs @@ -4,11 +4,51 @@ namespace OddsCollector.Functions.Notification.CommunicationServices.Configurati internal sealed class EmailSenderOptions { - [Required(AllowEmptyStrings = false)] public string Connection { get; set; } = string.Empty; + [Required(AllowEmptyStrings = false)] public string ConnectionString { get; set; } = string.Empty; [Required(AllowEmptyStrings = false)] public string RecipientAddress { get; set; } = string.Empty; [Required(AllowEmptyStrings = false)] public string SenderAddress { get; set; } = string.Empty; [Required(AllowEmptyStrings = false)] public string Subject { get; set; } = string.Empty; + + public void SetConnectionString(string? connectionString) + { + if (string.IsNullOrEmpty(connectionString)) + { + throw new ArgumentException($"{nameof(connectionString)} cannot be null or empty", nameof(connectionString)); + } + + ConnectionString = connectionString; + } + + public void SetRecipientAddress(string? recipientAddress) + { + if (string.IsNullOrEmpty(recipientAddress)) + { + throw new ArgumentException($"{nameof(recipientAddress)} cannot be null or empty", nameof(recipientAddress)); + } + + RecipientAddress = recipientAddress; + } + + public void SetSenderAddress(string? senderAddress) + { + if (string.IsNullOrEmpty(senderAddress)) + { + throw new ArgumentException($"{nameof(senderAddress)} cannot be null or empty", nameof(senderAddress)); + } + + SenderAddress = senderAddress; + } + + public void SetSubject(string? subject) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException($"{nameof(subject)} cannot be null or empty", nameof(subject)); + } + + Subject = subject; + } } diff --git a/OddsCollector.Functions.Notification/CommunicationServices/EmailSender.cs b/OddsCollector.Functions.Notification/CommunicationServices/EmailSender.cs index 205da96..fc890e2 100644 --- a/OddsCollector.Functions.Notification/CommunicationServices/EmailSender.cs +++ b/OddsCollector.Functions.Notification/CommunicationServices/EmailSender.cs @@ -25,7 +25,7 @@ public async Task SendEmailAsync(IEnumerable predictions) var content = JsonSerializer.Serialize(predictions, new JsonSerializerOptions { WriteIndented = true }); - var client = new EmailClient(_options.Connection); + var client = new EmailClient(_options.ConnectionString); await client.SendAsync(WaitUntil.Completed, _options.SenderAddress, _options.RecipientAddress, _options.Subject, content).ConfigureAwait(false); diff --git a/OddsCollector.Functions.Notification/CosmosDb/Configuration/CosmosDbOptions.cs b/OddsCollector.Functions.Notification/CosmosDb/Configuration/CosmosDbOptions.cs deleted file mode 100644 index 8d2df42..0000000 --- a/OddsCollector.Functions.Notification/CosmosDb/Configuration/CosmosDbOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OddsCollector.Functions.Notification.CosmosDb.Configuration; - -public class CosmosDbOptions -{ - [Required(AllowEmptyStrings = false)] public string Connection { get; set; } = string.Empty; - - [Required(AllowEmptyStrings = false)] public string Container { get; set; } = string.Empty; - - [Required(AllowEmptyStrings = false)] public string Database { get; set; } = string.Empty; -} diff --git a/OddsCollector.Functions.Notification/CosmosDb/ContainerFactory.cs b/OddsCollector.Functions.Notification/CosmosDb/ContainerFactory.cs new file mode 100644 index 0000000..bf206ce --- /dev/null +++ b/OddsCollector.Functions.Notification/CosmosDb/ContainerFactory.cs @@ -0,0 +1,26 @@ +using Microsoft.Azure.Cosmos; + +namespace OddsCollector.Functions.Notification.CosmosDb; + +internal static class ContainerFactory +{ + public static Container CreateContainer(string? connectionString, string? databaseId, string? containerId) + { + if (string.IsNullOrEmpty(connectionString)) + { + throw new ArgumentException($"{nameof(connectionString)} cannot be null or empty", nameof(connectionString)); + } + + if (string.IsNullOrEmpty(databaseId)) + { + throw new ArgumentException($"{nameof(databaseId)} cannot be null or empty", nameof(databaseId)); + } + + if (string.IsNullOrEmpty(containerId)) + { + throw new ArgumentException($"{nameof(containerId)} cannot be null or empty", nameof(containerId)); + } + + return new CosmosClient(connectionString).GetContainer(databaseId, containerId); + } +} diff --git a/OddsCollector.Functions.Notification/CosmosDb/CosmosDbClient.cs b/OddsCollector.Functions.Notification/CosmosDb/CosmosDbClient.cs index 584fa90..938a15e 100644 --- a/OddsCollector.Functions.Notification/CosmosDb/CosmosDbClient.cs +++ b/OddsCollector.Functions.Notification/CosmosDb/CosmosDbClient.cs @@ -1,8 +1,6 @@ using Microsoft.Azure.Cosmos; using Microsoft.Azure.Cosmos.Linq; -using Microsoft.Extensions.Options; using OddsCollector.Common.Models; -using OddsCollector.Functions.Notification.CosmosDb.Configuration; namespace OddsCollector.Functions.Notification.CosmosDb; @@ -10,10 +8,9 @@ internal sealed class CosmosDbClient : ICosmosDbClient { private readonly Container _container; - public CosmosDbClient(IOptions options) + public CosmosDbClient(Container container) { - var client = new CosmosClient(options.Value.Connection); - _container = client.GetContainer(options.Value.Database, options.Value.Container); + _container = container ?? throw new ArgumentNullException(nameof(container)); } public async Task> GetEventPredictionsAsync() diff --git a/OddsCollector.Functions.Notification/NotificationFunction.cs b/OddsCollector.Functions.Notification/NotificationFunction.cs index 3fe6ef2..8822a14 100644 --- a/OddsCollector.Functions.Notification/NotificationFunction.cs +++ b/OddsCollector.Functions.Notification/NotificationFunction.cs @@ -1,7 +1,12 @@ -using Microsoft.Azure.Functions.Worker; +using System.Runtime.CompilerServices; +using Microsoft.Azure.Functions.Worker; using OddsCollector.Functions.Notification.CommunicationServices; using OddsCollector.Functions.Notification.CosmosDb; +[assembly: InternalsVisibleTo("OddsCollector.Functions.Notification.Tests")] +// DynamicProxyGenAssembly2 is a temporary assembly built by mocking systems that use CastleProxy +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] + namespace OddsCollector.Functions.Notification; internal sealed class NotificationFunction diff --git a/OddsCollector.Functions.Notification/Program.cs b/OddsCollector.Functions.Notification/Program.cs index 6eb571d..01e71dd 100644 --- a/OddsCollector.Functions.Notification/Program.cs +++ b/OddsCollector.Functions.Notification/Program.cs @@ -4,27 +4,26 @@ using OddsCollector.Functions.Notification.CommunicationServices; using OddsCollector.Functions.Notification.CommunicationServices.Configuration; using OddsCollector.Functions.Notification.CosmosDb; -using OddsCollector.Functions.Notification.CosmosDb.Configuration; var host = new HostBuilder() .ConfigureFunctionsWorkerDefaults() .ConfigureServices(services => { - // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 - services.Configure(o => - { - o.Container = Environment.GetEnvironmentVariable("CosmosDb:Container"); - o.Database = Environment.GetEnvironmentVariable("CosmosDb:Database"); - o.Connection = Environment.GetEnvironmentVariable("CosmosDb:Connection"); - }); - // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 services.Configure(o => { - o.Subject = Environment.GetEnvironmentVariable("EmailSender:Subject"); - o.Connection = Environment.GetEnvironmentVariable("EmailSender:Connection"); - o.RecipientAddress = Environment.GetEnvironmentVariable("EmailSender:RecipientAddress"); - o.SenderAddress = Environment.GetEnvironmentVariable("EmailSender:SenderAddress"); + // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 + o.SetSubject(Environment.GetEnvironmentVariable("EmailSender:Subject")); + o.SetConnectionString(Environment.GetEnvironmentVariable("EmailSender:Connection")); + o.SetRecipientAddress(Environment.GetEnvironmentVariable("EmailSender:RecipientAddress")); + o.SetSenderAddress(Environment.GetEnvironmentVariable("EmailSender:SenderAddress")); }); + services.AddSingleton( + ContainerFactory.CreateContainer( + // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 + Environment.GetEnvironmentVariable("CosmosDb:Connection"), + Environment.GetEnvironmentVariable("CosmosDb:Database"), + Environment.GetEnvironmentVariable("CosmosDb:Container") + )); services.AddSingleton(); services.AddSingleton(); services.AddApplicationInsightsTelemetryWorkerService(); diff --git a/OddsCollector.Functions.UpcomingEvents/Program.cs b/OddsCollector.Functions.UpcomingEvents/Program.cs index 1602a5c..31fd818 100644 --- a/OddsCollector.Functions.UpcomingEvents/Program.cs +++ b/OddsCollector.Functions.UpcomingEvents/Program.cs @@ -15,7 +15,7 @@ services.Configure(o => { // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 - o.Leagues = LeaguesFactory.CreateLeagues(Environment.GetEnvironmentVariable("OddsApi:Leagues")); + o.SetLeagues(Environment.GetEnvironmentVariable("OddsApi:Leagues")); }); services.AddHttpClient(); services.AddSingleton(); diff --git a/OddsCollector.sln b/OddsCollector.sln index 5ee5384..1e13358 100644 --- a/OddsCollector.sln +++ b/OddsCollector.sln @@ -15,11 +15,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OddsCollector.Functions.Eve EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OddsCollector.Common.Tests", "OddsCollector.Common.Tests\OddsCollector.Common.Tests.csproj", "{0A5F1BC2-027B-4474-9C7E-6B00C8CCF7CD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OddsCollector.Functions.EventResults.Tests", "OddsCollector.Functions.EventResults.Tests\OddsCollector.Functions.EventResults.Tests.csproj", "{406DE1F4-069B-4189-8AA2-994CC1140BC6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OddsCollector.Functions.EventResults.Tests", "OddsCollector.Functions.EventResults.Tests\OddsCollector.Functions.EventResults.Tests.csproj", "{406DE1F4-069B-4189-8AA2-994CC1140BC6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OddsCollector.Functions.UpcomingEvents.Tests", "OddsCollector.Functions.UpcomingEvents.Tests\OddsCollector.Functions.UpcomingEvents.Tests.csproj", "{D2E01FC2-A174-40FC-BC56-6EE3FD640AE1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OddsCollector.Functions.UpcomingEvents.Tests", "OddsCollector.Functions.UpcomingEvents.Tests\OddsCollector.Functions.UpcomingEvents.Tests.csproj", "{D2E01FC2-A174-40FC-BC56-6EE3FD640AE1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OddsCollector.Functions.Predictions.Tests", "OddsCollector.Functions.Predictions.Tests\OddsCollector.Functions.Predictions.Tests.csproj", "{296C1955-19D3-49E5-A4E5-E68424525E8E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OddsCollector.Functions.Predictions.Tests", "OddsCollector.Functions.Predictions.Tests\OddsCollector.Functions.Predictions.Tests.csproj", "{296C1955-19D3-49E5-A4E5-E68424525E8E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OddsCollector.Functions.Notification.Tests", "OddsCollector.Functions.Notification.Tests\OddsCollector.Functions.Notification.Tests.csproj", "{687A2464-8366-4077-8AE6-00F6E3D40905}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -63,6 +65,10 @@ Global {296C1955-19D3-49E5-A4E5-E68424525E8E}.Debug|Any CPU.Build.0 = Debug|Any CPU {296C1955-19D3-49E5-A4E5-E68424525E8E}.Release|Any CPU.ActiveCfg = Release|Any CPU {296C1955-19D3-49E5-A4E5-E68424525E8E}.Release|Any CPU.Build.0 = Release|Any CPU + {687A2464-8366-4077-8AE6-00F6E3D40905}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {687A2464-8366-4077-8AE6-00F6E3D40905}.Debug|Any CPU.Build.0 = Debug|Any CPU + {687A2464-8366-4077-8AE6-00F6E3D40905}.Release|Any CPU.ActiveCfg = Release|Any CPU + {687A2464-8366-4077-8AE6-00F6E3D40905}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE