diff --git a/OddsCollector.Common.Tests/OddsApi/Configuration/OddsApiOptionsFactoryTests.cs b/OddsCollector.Common.Tests/OddsApi/Configuration/OddsApiOptionsFactoryTests.cs new file mode 100644 index 0000000..9deb98f --- /dev/null +++ b/OddsCollector.Common.Tests/OddsApi/Configuration/OddsApiOptionsFactoryTests.cs @@ -0,0 +1,56 @@ +using FluentAssertions; +using OddsCollector.Common.OddsApi.Configuration; + +namespace OddsCollector.Common.Tests.OddsApi.Configuration; + +internal sealed class OddsApiOptionsFactoryTests +{ + [Test] + public void CreateOddsApiOptions_WithValidLeague_ReturnsNewInstance() + { + string league = nameof(league); + + var options = OddsApiOptionsFactory.CreateOddsApiOptions(league); + + options.Should().NotBeNull(); + options.Leagues.Should().NotBeNull().And.HaveCount(1); + options.Leagues.ElementAt(0).Should().NotBeNull().And.Be(league); + } + + [Test] + public void CreateOddsApiOptions_WithValidLeagues_ReturnsNewInstance() + { + var leagues = "league1;league2"; + + var options = OddsApiOptionsFactory.CreateOddsApiOptions(leagues); + + options.Should().NotBeNull(); + 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 CreateOddsApiOptions_WithDuplicatedLeagues_ReturnsNewInstance() + { + var leagues = "league1;league1"; + + var options = OddsApiOptionsFactory.CreateOddsApiOptions(leagues); + + options.Should().NotBeNull(); + options.Leagues.Should().NotBeNull().And.HaveCount(1); + options.Leagues.ElementAt(0).Should().NotBeNull().And.Be("league1"); + } + + [TestCase("")] + [TestCase(null)] + public void CreateOddsApiOptions_WithNullOrEmptyLeagues_ThrowsException(string? leagues) + { + var action = () => + { + _ = OddsApiOptionsFactory.CreateOddsApiOptions(leagues); + }; + + action.Should().Throw().WithParameterName(nameof(leagues)); + } +} diff --git a/OddsCollector.Common.Tests/OddsApi/Converter/RawUpcomingEventBuilder.cs b/OddsCollector.Common.Tests/OddsApi/Converter/Anonymous2Builder.cs similarity index 69% rename from OddsCollector.Common.Tests/OddsApi/Converter/RawUpcomingEventBuilder.cs rename to OddsCollector.Common.Tests/OddsApi/Converter/Anonymous2Builder.cs index 3e968e5..fe92c3f 100644 --- a/OddsCollector.Common.Tests/OddsApi/Converter/RawUpcomingEventBuilder.cs +++ b/OddsCollector.Common.Tests/OddsApi/Converter/Anonymous2Builder.cs @@ -2,7 +2,7 @@ namespace OddsCollector.Common.Tests.OddsApi.Converter; -internal class RawUpcomingEventBuilder +internal class Anonymous2Builder { public const string DefaultAwayTeam = "Liverpool"; public const string DefaultHomeTeam = "Manchester City"; @@ -47,59 +47,51 @@ internal class RawUpcomingEventBuilder } }; - private Anonymous2 _state = new(); + private readonly Anonymous2 _state = new(); - public RawUpcomingEventBuilder SetDefaults() + public Anonymous2Builder SetDefaults() { - _state = new Anonymous2 - { - Away_team = DefaultAwayTeam, - Commence_time = DefaultCommenceTime, - Home_team = DefaultHomeTeam, - Id = DefaultId, - Bookmakers = DefaultBookmakers - }; - - return this; + return SetAwayTeam(DefaultAwayTeam) + .SetCommenceTime(DefaultCommenceTime) + .SetHomeTeam(DefaultHomeTeam) + .SetId(DefaultId) + .SetBookmakers(DefaultBookmakers); } - public RawUpcomingEventBuilder SetAwayTeam(string? awayTeam) + public Anonymous2Builder SetAwayTeam(string? awayTeam) { _state.Away_team = awayTeam; return this; } - public RawUpcomingEventBuilder SetHomeTeam(string? homeTeam) + public Anonymous2Builder SetHomeTeam(string? homeTeam) { _state.Home_team = homeTeam; return this; } - public RawUpcomingEventBuilder SetId(string id) + public Anonymous2Builder SetId(string id) { _state.Id = id; return this; } - public RawUpcomingEventBuilder SetCommenceTime(DateTime commenceTime) + public Anonymous2Builder SetCommenceTime(DateTime commenceTime) { _state.Commence_time = commenceTime; return this; } - public RawUpcomingEventBuilder SetBookmakers(ICollection? bookmakers) + public Anonymous2Builder SetBookmakers(ICollection? bookmakers) { _state.Bookmakers = bookmakers; return this; } - public Anonymous2 GetRawUpcomingEvent() - { - return _state; - } + public Anonymous2 Instance => _state; } diff --git a/OddsCollector.Common.Tests/OddsApi/Converter/RawEventResultBuilder.cs b/OddsCollector.Common.Tests/OddsApi/Converter/Anonymous3Builder.cs similarity index 54% rename from OddsCollector.Common.Tests/OddsApi/Converter/RawEventResultBuilder.cs rename to OddsCollector.Common.Tests/OddsApi/Converter/Anonymous3Builder.cs index e721d58..a55969a 100644 --- a/OddsCollector.Common.Tests/OddsApi/Converter/RawEventResultBuilder.cs +++ b/OddsCollector.Common.Tests/OddsApi/Converter/Anonymous3Builder.cs @@ -2,7 +2,7 @@ namespace OddsCollector.Common.Tests.OddsApi.Converter; -internal sealed class RawEventResultBuilder +internal sealed class Anonymous3Builder { public const string DefaultAwayTeam = "Liverpool"; public const bool DefaultCompleted = true; @@ -15,67 +15,59 @@ internal sealed class RawEventResultBuilder new() { Name = "Manchester City", Score = "1" }, new() { Name = "Liverpool", Score = "0" } }; - private Anonymous3 _state = new(); + private readonly Anonymous3 _state = new(); - public RawEventResultBuilder SetDefaults() + public Anonymous3Builder SetDefaults() { - _state = new Anonymous3 - { - Away_team = DefaultAwayTeam, - Completed = DefaultCompleted, - Home_team = DefaultHomeTeam, - Id = DefaultId, - Commence_time = DefaultCommenceTime, - Scores = DefaultScores - }; - - return this; + return SetId(DefaultId) + .SetAwayTeam(DefaultAwayTeam) + .SetCompleted(DefaultCompleted) + .SetHomeTeam(DefaultHomeTeam) + .SetCommenceTime(DefaultCommenceTime) + .SetScores(DefaultScores); } - public RawEventResultBuilder SetAwayTeam(string? awayTeam) + public Anonymous3Builder SetAwayTeam(string? awayTeam) { _state.Away_team = awayTeam; return this; } - public RawEventResultBuilder SetCompleted(bool? completed) + public Anonymous3Builder SetCompleted(bool? completed) { _state.Completed = completed; return this; } - public RawEventResultBuilder SetHomeTeam(string? homeTeam) + public Anonymous3Builder SetHomeTeam(string? homeTeam) { _state.Home_team = homeTeam; return this; } - public RawEventResultBuilder SetId(string id) + public Anonymous3Builder SetId(string id) { _state.Id = id; return this; } - public RawEventResultBuilder SetCommenceTime(DateTime commenceTime) + public Anonymous3Builder SetCommenceTime(DateTime commenceTime) { _state.Commence_time = commenceTime; return this; } - public RawEventResultBuilder SetScores(ICollection? scores) + public Anonymous3Builder SetScores(ICollection? scores) { _state.Scores = scores; return this; } - public Anonymous3 GetRawEventResult() - { - return _state; - } + public Anonymous3 Instance => _state; } diff --git a/OddsCollector.Common.Tests/OddsApi/Converter/OddsApiObjectConverterTests.cs b/OddsCollector.Common.Tests/OddsApi/Converter/OddsApiObjectConverterTests.cs index 0c8c60e..c7d932c 100644 --- a/OddsCollector.Common.Tests/OddsApi/Converter/OddsApiObjectConverterTests.cs +++ b/OddsCollector.Common.Tests/OddsApi/Converter/OddsApiObjectConverterTests.cs @@ -17,9 +17,8 @@ public void ToUpcomingEvents_WithOddList_ReturnsConvertedEvents() var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().GetRawUpcomingEvent(), - new RawUpcomingEventBuilder().SetDefaults().SetId("1766194919f1cbfbd846576434f0499b") - .GetRawUpcomingEvent() + new Anonymous2Builder().SetDefaults().Instance, + new Anonymous2Builder().SetDefaults().SetId("1766194919f1cbfbd846576434f0499b").Instance }; var upcomingEvents = converter.ToUpcomingEvents(rawUpcomingEvents, traceId, timestamp); @@ -29,10 +28,10 @@ public void ToUpcomingEvents_WithOddList_ReturnsConvertedEvents() var firstEvent = upcomingEvents.ElementAt(0); firstEvent.Should().NotBeNull(); - firstEvent.AwayTeam.Should().NotBeNull().And.Be(RawUpcomingEventBuilder.DefaultAwayTeam); - firstEvent.CommenceTime.Should().NotBeNull().And.Be(RawUpcomingEventBuilder.DefaultCommenceTime); - firstEvent.HomeTeam.Should().NotBeNull().And.Be(RawUpcomingEventBuilder.DefaultHomeTeam); - firstEvent.Id.Should().NotBeNull().And.Be(RawUpcomingEventBuilder.DefaultId); + firstEvent.AwayTeam.Should().NotBeNull().And.Be(Anonymous2Builder.DefaultAwayTeam); + firstEvent.CommenceTime.Should().NotBeNull().And.Be(Anonymous2Builder.DefaultCommenceTime); + firstEvent.HomeTeam.Should().NotBeNull().And.Be(Anonymous2Builder.DefaultHomeTeam); + firstEvent.Id.Should().NotBeNull().And.Be(Anonymous2Builder.DefaultId); firstEvent.Timestamp.Should().NotBeNull().And.Be(timestamp); firstEvent.TraceId.Should().NotBeNull().And.Be(traceId); firstEvent.Odds.Should().NotBeNull().And.HaveCount(2); @@ -50,9 +49,9 @@ public void ToUpcomingEvents_WithOddList_ReturnsConvertedEvents() var secondEvent = upcomingEvents.ElementAt(1); secondEvent.Should().NotBeNull(); - secondEvent.AwayTeam.Should().NotBeNull().And.Be(RawUpcomingEventBuilder.DefaultAwayTeam); - secondEvent.CommenceTime.Should().NotBeNull().And.Be(RawUpcomingEventBuilder.DefaultCommenceTime); - secondEvent.HomeTeam.Should().NotBeNull().And.Be(RawUpcomingEventBuilder.DefaultHomeTeam); + secondEvent.AwayTeam.Should().NotBeNull().And.Be(Anonymous2Builder.DefaultAwayTeam); + secondEvent.CommenceTime.Should().NotBeNull().And.Be(Anonymous2Builder.DefaultCommenceTime); + secondEvent.HomeTeam.Should().NotBeNull().And.Be(Anonymous2Builder.DefaultHomeTeam); secondEvent.Id.Should().NotBeNull().And.Be("1766194919f1cbfbd846576434f0499b"); secondEvent.Timestamp.Should().NotBeNull().And.Be(timestamp); secondEvent.TraceId.Should().NotBeNull().And.Be(traceId); @@ -118,7 +117,7 @@ public void ToUpcomingEvents_WithNullBookmakers_ThrowsException() var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().SetBookmakers(null).GetRawUpcomingEvent() + new Anonymous2Builder().SetDefaults().SetBookmakers(null).Instance }; var action = () => @@ -137,7 +136,7 @@ public void ToUpcomingEvents_WithNullOrEmptyAwayTeam_ThrowsException(string? awa var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().SetAwayTeam(awayTeam).GetRawUpcomingEvent() + new Anonymous2Builder().SetDefaults().SetAwayTeam(awayTeam).Instance }; var action = () => @@ -156,7 +155,7 @@ public void ToUpcomingEvents_WithNullOrEmptyHomeTeam_ThrowsException(string? hom var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().SetHomeTeam(homeTeam).GetRawUpcomingEvent() + new Anonymous2Builder().SetDefaults().SetHomeTeam(homeTeam).Instance }; var action = () => @@ -175,7 +174,7 @@ public void ToUpcomingEvents_WithNullOrEmptyBookmakerKey_ThrowsException(string? var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().SetBookmakers( + new Anonymous2Builder().SetDefaults().SetBookmakers( new List { new() @@ -196,7 +195,7 @@ public void ToUpcomingEvents_WithNullOrEmptyBookmakerKey_ThrowsException(string? } } } - ).GetRawUpcomingEvent() + ).Instance }; var action = () => @@ -214,9 +213,9 @@ public void ToUpcomingEvents_WithNullMarkets_ThrowsException() var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().SetBookmakers( + new Anonymous2Builder().SetDefaults().SetBookmakers( new List { new() { Key = "onexbet", Markets = null } } - ).GetRawUpcomingEvent() + ).Instance }; var action = () => @@ -234,7 +233,7 @@ public void ToUpcomingEvents_WithNullMarketKey_ThrowsException() var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().SetBookmakers( + new Anonymous2Builder().SetDefaults().SetBookmakers( new List { new() @@ -255,7 +254,7 @@ public void ToUpcomingEvents_WithNullMarketKey_ThrowsException() } } } - ).GetRawUpcomingEvent() + ).Instance }; var action = () => @@ -273,9 +272,9 @@ public void ToUpcomingEvents_WithEmptyMarkets_ThrowsException() var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().SetBookmakers( + new Anonymous2Builder().SetDefaults().SetBookmakers( new List { new() { Key = "onexbet", Markets = new List() } } - ).GetRawUpcomingEvent() + ).Instance }; var action = () => @@ -293,7 +292,7 @@ public void ToUpcomingEvents_WithNullOutcomes_ThrowsException() var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().SetBookmakers( + new Anonymous2Builder().SetDefaults().SetBookmakers( new List { new() @@ -302,7 +301,7 @@ public void ToUpcomingEvents_WithNullOutcomes_ThrowsException() Markets = new List { new() { Key = Markets2Key.H2h, Outcomes = null } } } } - ).GetRawUpcomingEvent() + ).Instance }; var action = () => @@ -320,7 +319,7 @@ public void ToUpcomingEvents_WithEmptyOutcomes_ThrowsException() var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().SetBookmakers( + new Anonymous2Builder().SetDefaults().SetBookmakers( new List { new() @@ -332,7 +331,7 @@ public void ToUpcomingEvents_WithEmptyOutcomes_ThrowsException() } } } - ).GetRawUpcomingEvent() + ).Instance }; var action = () => @@ -350,7 +349,7 @@ public void ToUpcomingEvents_WithOneOutcome_ThrowsException() var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().SetBookmakers( + new Anonymous2Builder().SetDefaults().SetBookmakers( new List { new() @@ -370,7 +369,7 @@ public void ToUpcomingEvents_WithOneOutcome_ThrowsException() } } } - ).GetRawUpcomingEvent() + ).Instance }; var action = () => @@ -382,13 +381,13 @@ public void ToUpcomingEvents_WithOneOutcome_ThrowsException() } [Test] - public void ToUpcomingEvents_WithNullPrice_ThrowsException() + public void ToUpcomingEvents_WithNullHomePrice_ThrowsException() { var converter = new OddsApiObjectConverter(); var rawUpcomingEvents = new List { - new RawUpcomingEventBuilder().SetDefaults().SetBookmakers( + new Anonymous2Builder().SetDefaults().SetBookmakers( new List { new() @@ -401,15 +400,93 @@ public void ToUpcomingEvents_WithNullPrice_ThrowsException() Key = Markets2Key.H2h, Outcomes = new List { - new() { Name = "Liverpool", Price = null }, + new() { Name = "Liverpool", Price = 4.08 }, new() { Name = "Manchester City", Price = null }, + new() { Name = "Draw", Price = 3.82 } + } + } + } + } + } + ).Instance + }; + + var action = () => + { + _ = converter.ToUpcomingEvents(rawUpcomingEvents, Guid.NewGuid(), DateTime.UtcNow).ToList(); + }; + + action.Should().Throw().WithParameterName("home"); + } + + [Test] + public void ToUpcomingEvents_WithNullAwayPrice_ThrowsException() + { + var converter = new OddsApiObjectConverter(); + + var rawUpcomingEvents = new List + { + new Anonymous2Builder().SetDefaults().SetBookmakers( + new List + { + new() + { + Key = "onexbet", + Markets = new List + { + new() + { + Key = Markets2Key.H2h, + Outcomes = new List + { + new() { Name = "Liverpool", Price = null }, + new() { Name = "Manchester City", Price = 1.7 }, + new() { Name = "Draw", Price = 3.82 } + } + } + } + } + } + ).Instance + }; + + var action = () => + { + _ = converter.ToUpcomingEvents(rawUpcomingEvents, Guid.NewGuid(), DateTime.UtcNow).ToList(); + }; + + action.Should().Throw().WithParameterName("away"); + } + + [Test] + public void ToUpcomingEvents_WithNullDrawPrice_ThrowsException() + { + var converter = new OddsApiObjectConverter(); + + var rawUpcomingEvents = new List + { + new Anonymous2Builder().SetDefaults().SetBookmakers( + new List + { + new() + { + Key = "onexbet", + Markets = new List + { + new() + { + Key = Markets2Key.H2h, + Outcomes = new List + { + new() { Name = "Liverpool", Price = 4.08 }, + new() { Name = "Manchester City", Price = 1.7 }, new() { Name = "Draw", Price = null } } } } } } - ).GetRawUpcomingEvent() + ).Instance }; var action = () => @@ -417,7 +494,7 @@ public void ToUpcomingEvents_WithNullPrice_ThrowsException() _ = converter.ToUpcomingEvents(rawUpcomingEvents, Guid.NewGuid(), DateTime.UtcNow).ToList(); }; - action.Should().Throw().WithParameterName("outcomes"); + action.Should().Throw().WithParameterName("draw"); } [Test] @@ -430,18 +507,18 @@ public void ToEventResults_WithCompletedEvents_ReturnsConvertedEvents() var rawEventResults = new List { - new RawEventResultBuilder().SetDefaults().SetScores(new List + new Anonymous3Builder().SetDefaults().SetScores(new List { new() { Name = "Manchester City", Score = "1" }, new() { Name = "Liverpool", Score = "0" } - }).GetRawEventResult(), - new RawEventResultBuilder().SetDefaults().SetScores(new List + }).Instance, + new Anonymous3Builder().SetDefaults().SetScores(new List { new() { Name = "Manchester City", Score = "0" }, new() { Name = "Liverpool", Score = "1" } - }).GetRawEventResult(), - new RawEventResultBuilder().SetDefaults().SetScores(new List + }).Instance, + new Anonymous3Builder().SetDefaults().SetScores(new List { new() { Name = "Manchester City", Score = "1" }, new() { Name = "Liverpool", Score = "1" } - }).GetRawEventResult() + }).Instance }; var results = converter.ToEventResults(rawEventResults, traceId, timestamp); @@ -451,8 +528,8 @@ public void ToEventResults_WithCompletedEvents_ReturnsConvertedEvents() var firstResult = results.ElementAt(0); firstResult.Should().NotBeNull(); - firstResult.CommenceTime.Should().NotBeNull().And.Be(RawEventResultBuilder.DefaultCommenceTime); - firstResult.Id.Should().NotBeNull().And.Be(RawEventResultBuilder.DefaultId); + firstResult.CommenceTime.Should().NotBeNull().And.Be(Anonymous3Builder.DefaultCommenceTime); + firstResult.Id.Should().NotBeNull().And.Be(Anonymous3Builder.DefaultId); firstResult.Timestamp.Should().NotBeNull().And.Be(timestamp); firstResult.TraceId.Should().NotBeNull().And.Be(traceId); firstResult.Winner.Should().NotBeNull().And.Be("Manchester City"); @@ -460,8 +537,8 @@ public void ToEventResults_WithCompletedEvents_ReturnsConvertedEvents() var secondResult = results.ElementAt(1); secondResult.Should().NotBeNull(); - secondResult.CommenceTime.Should().NotBeNull().And.Be(RawEventResultBuilder.DefaultCommenceTime); - secondResult.Id.Should().NotBeNull().And.Be(RawEventResultBuilder.DefaultId); + secondResult.CommenceTime.Should().NotBeNull().And.Be(Anonymous3Builder.DefaultCommenceTime); + secondResult.Id.Should().NotBeNull().And.Be(Anonymous3Builder.DefaultId); secondResult.Timestamp.Should().NotBeNull().And.Be(timestamp); secondResult.TraceId.Should().NotBeNull().And.Be(traceId); secondResult.Winner.Should().NotBeNull().And.Be("Liverpool"); @@ -469,8 +546,8 @@ public void ToEventResults_WithCompletedEvents_ReturnsConvertedEvents() var thirdResult = results.ElementAt(2); thirdResult.Should().NotBeNull(); - thirdResult.CommenceTime.Should().NotBeNull().And.Be(RawEventResultBuilder.DefaultCommenceTime); - thirdResult.Id.Should().NotBeNull().And.Be(RawEventResultBuilder.DefaultId); + thirdResult.CommenceTime.Should().NotBeNull().And.Be(Anonymous3Builder.DefaultCommenceTime); + thirdResult.Id.Should().NotBeNull().And.Be(Anonymous3Builder.DefaultId); thirdResult.Timestamp.Should().NotBeNull().And.Be(timestamp); thirdResult.TraceId.Should().NotBeNull().And.Be(traceId); thirdResult.Winner.Should().NotBeNull().And.Be(Constants.Draw); @@ -508,8 +585,8 @@ public void ToEventResults_WithUncompletedEvents_ReturnsNoEvents() var rawEventResults = new List { - new RawEventResultBuilder().SetDefaults().SetCompleted(null).GetRawEventResult(), - new RawEventResultBuilder().SetDefaults().SetCompleted(false).GetRawEventResult() + new Anonymous3Builder().SetDefaults().SetCompleted(null).Instance, + new Anonymous3Builder().SetDefaults().SetCompleted(false).Instance }; var results = converter.ToEventResults(rawEventResults, Guid.NewGuid(), DateTime.UtcNow); @@ -536,7 +613,7 @@ public void ToEventResults_WithEmptyScores_ThrowsException() var rawEventResults = new List { - new RawEventResultBuilder().SetDefaults().SetScores(null).GetRawEventResult() + new Anonymous3Builder().SetDefaults().SetScores(null).Instance }; var action = () => @@ -555,7 +632,7 @@ public void ToEventResults_WithNullOrEmptyHomeTeam_ThrowsException(string? homeT var rawEventResults = new List { - new RawEventResultBuilder().SetDefaults().SetHomeTeam(homeTeam).GetRawEventResult() + new Anonymous3Builder().SetDefaults().SetHomeTeam(homeTeam).Instance }; var action = () => @@ -574,7 +651,7 @@ public void ToEventResults_WithNullOrEmptyAwayTeam_ThrowsException(string? awayT var rawEventResults = new List { - new RawEventResultBuilder().SetDefaults().SetAwayTeam(awayTeam).GetRawEventResult() + new Anonymous3Builder().SetDefaults().SetAwayTeam(awayTeam).Instance }; var action = () => @@ -593,10 +670,10 @@ public void ToEventResults_WithNullOrEmptyTeamName_ThrowsException(string? name) var rawEventResults = new List { - new RawEventResultBuilder().SetDefaults().SetScores(new List + new Anonymous3Builder().SetDefaults().SetScores(new List { new() { Name = name, Score = "1" }, new() { Name = name, Score = "1" } - }).GetRawEventResult() + }).Instance }; var action = () => @@ -615,10 +692,10 @@ public void ToEventResults_WithNullOrEmptyScoreValue_ThrowsException(string? sco var rawEventResults = new List { - new RawEventResultBuilder().SetDefaults().SetScores(new List + new Anonymous3Builder().SetDefaults().SetScores(new List { new() { Name = "Manchester City", Score = score }, new() { Name = "Liverpool", Score = score } - }).GetRawEventResult() + }).Instance }; var action = () => @@ -636,12 +713,12 @@ public void ToEventResults_WithDuplicatedScore_ThrowsException() var rawEventResults = new List { - new RawEventResultBuilder().SetDefaults().SetScores(new List + new Anonymous3Builder().SetDefaults().SetScores(new List { new() { Name = "Manchester City", Score = "1" }, new() { Name = "Liverpool", Score = "1" }, new() { Name = "Liverpool", Score = "1" } - }).GetRawEventResult() + }).Instance }; var action = () => @@ -659,12 +736,12 @@ public void ToEventResults_WithExtraScore_ReturnsEventResult() var rawEventResults = new List { - new RawEventResultBuilder().SetDefaults().SetScores(new List + new Anonymous3Builder().SetDefaults().SetScores(new List { new() { Name = "Manchester City", Score = "1" }, new() { Name = "Liverpool", Score = "0" }, new() { Name = "Nottingham Forest", Score = "1" } - }).GetRawEventResult() + }).Instance }; var result = converter.ToEventResults(rawEventResults, Guid.NewGuid(), DateTime.UtcNow).ToList(); @@ -681,10 +758,10 @@ public void ToEventResults_WithoutAwayTeamScore_ThrowsException() var rawEventResults = new List { - new RawEventResultBuilder().SetDefaults().SetScores(new List + new Anonymous3Builder().SetDefaults().SetScores(new List { new() { Name = "Manchester City", Score = "1" } - }).GetRawEventResult() + }).Instance }; var action = () => @@ -702,10 +779,10 @@ public void ToEventResults_WithoutHomeTeamScore_ThrowsException() var rawEventResults = new List { - new RawEventResultBuilder().SetDefaults().SetScores(new List + new Anonymous3Builder().SetDefaults().SetScores(new List { new() { Name = "Liverpool", Score = "1" } - }).GetRawEventResult() + }).Instance }; var action = () => diff --git a/OddsCollector.Common.Tests/OddsCollector.Common.Tests.csproj b/OddsCollector.Common.Tests/OddsCollector.Common.Tests.csproj index 049ea7d..581d60f 100644 --- a/OddsCollector.Common.Tests/OddsCollector.Common.Tests.csproj +++ b/OddsCollector.Common.Tests/OddsCollector.Common.Tests.csproj @@ -10,11 +10,11 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,7 +26,7 @@ - + diff --git a/OddsCollector.Common/Models/EventPrediction.cs b/OddsCollector.Common/Models/EventPrediction.cs index 31034e3..16b26a5 100644 --- a/OddsCollector.Common/Models/EventPrediction.cs +++ b/OddsCollector.Common/Models/EventPrediction.cs @@ -5,20 +5,20 @@ namespace OddsCollector.Common.Models; public class EventPrediction { // duplicating information to avoid complex queries to cosmosdb - public string? AwayTeam { get; init; } + public string? AwayTeam { get; set; } - public string? Bookmaker { get; init; } + public string? Bookmaker { get; set; } // duplicating information to avoid complex queries to cosmosdb - public DateTime? CommenceTime { get; init; } + public DateTime? CommenceTime { get; set; } // duplicating information to avoid complex queries to cosmosdb - public string? HomeTeam { get; init; } + public string? HomeTeam { get; set; } // fix for cosmosdb - [JsonPropertyName("id")] public string? Id { get; init; } - public string? Strategy { get; init; } - public DateTime? Timestamp { get; init; } - public Guid? TraceId { get; init; } - public string? Winner { get; init; } + [JsonPropertyName("id")] public string? Id { get; set; } + public string? Strategy { get; set; } + public DateTime? Timestamp { get; set; } + public Guid? TraceId { get; set; } + public string? Winner { get; set; } } diff --git a/OddsCollector.Common/Models/EventPredictionBuilder.cs b/OddsCollector.Common/Models/EventPredictionBuilder.cs new file mode 100644 index 0000000..ff766bb --- /dev/null +++ b/OddsCollector.Common/Models/EventPredictionBuilder.cs @@ -0,0 +1,116 @@ +namespace OddsCollector.Common.Models; + +public class EventPredictionBuilder +{ + private readonly EventPrediction _state = new(); + + public EventPredictionBuilder SetAwayTeam(string? awayTeam) + { + if (string.IsNullOrEmpty(awayTeam)) + { + throw new ArgumentException($"{nameof(awayTeam)} cannot be null or empty", nameof(awayTeam)); + } + + _state.AwayTeam = awayTeam; + + return this; + } + + public EventPredictionBuilder SetBookmaker(string? bookmaker) + { + if (string.IsNullOrEmpty(bookmaker)) + { + throw new ArgumentException($"{nameof(bookmaker)} cannot be null or empty", nameof(bookmaker)); + } + + _state.Bookmaker = bookmaker; + + return this; + } + + public EventPredictionBuilder SetCommenceTime(DateTime? commenceTime) + { + if (commenceTime is null) + { + throw new ArgumentNullException(nameof(commenceTime)); + } + + _state.CommenceTime = commenceTime; + + return this; + } + + public EventPredictionBuilder SetHomeTeam(string? homeTeam) + { + if (string.IsNullOrEmpty(homeTeam)) + { + throw new ArgumentException($"{nameof(homeTeam)} cannot be null or empty", nameof(homeTeam)); + } + + _state.HomeTeam = homeTeam; + + return this; + } + + public EventPredictionBuilder SetId(string? id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentException($"{nameof(id)} cannot be null or empty", nameof(id)); + } + + _state.Id = id; + + return this; + } + + public EventPredictionBuilder SetStrategy(string? strategy) + { + if (string.IsNullOrEmpty(strategy)) + { + throw new ArgumentException($"{nameof(strategy)} cannot be null or empty", nameof(strategy)); + } + + _state.Strategy = strategy; + + return this; + } + + public EventPredictionBuilder SetTimestamp(DateTime? timestamp) + { + if (timestamp is null) + { + throw new ArgumentNullException(nameof(timestamp)); + } + + _state.Timestamp = timestamp; + + return this; + } + + public EventPredictionBuilder SetTraceId(Guid? traceId) + { + if (traceId is null) + { + throw new ArgumentNullException(nameof(traceId)); + } + + _state.TraceId = traceId; + + return this; + } + + public EventPredictionBuilder SetWinner(string? winner) + { + if (string.IsNullOrEmpty(winner)) + { + throw new ArgumentException($"{nameof(winner)} cannot be null or empty", nameof(winner)); + } + + _state.Winner = winner; + + return this; + } + + public EventPrediction Instance => _state; +} diff --git a/OddsCollector.Common/Models/EventResult.cs b/OddsCollector.Common/Models/EventResult.cs index 203241d..10d6ee3 100644 --- a/OddsCollector.Common/Models/EventResult.cs +++ b/OddsCollector.Common/Models/EventResult.cs @@ -4,11 +4,11 @@ namespace OddsCollector.Common.Models; public class EventResult { - public DateTime? CommenceTime { get; init; } + public DateTime? CommenceTime { get; set; } // fix for cosmosdb - [JsonPropertyName("id")] public string? Id { get; init; } - public DateTime? Timestamp { get; init; } - public Guid? TraceId { get; init; } - public string? Winner { get; init; } + [JsonPropertyName("id")] public string? Id { get; set; } + public DateTime? Timestamp { get; set; } + public Guid? TraceId { get; set; } + public string? Winner { get; set; } } diff --git a/OddsCollector.Common/Models/EventResultBuilder.cs b/OddsCollector.Common/Models/EventResultBuilder.cs new file mode 100644 index 0000000..7d50ccc --- /dev/null +++ b/OddsCollector.Common/Models/EventResultBuilder.cs @@ -0,0 +1,68 @@ +namespace OddsCollector.Common.Models; + +public class EventResultBuilder +{ + private readonly EventResult _state = new(); + + public EventResultBuilder SetId(string? id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentException($"{nameof(id)} cannot be null or empty", nameof(id)); + } + + _state.Id = id; + + return this; + } + + public EventResultBuilder SetWinner(string? winner) + { + if (string.IsNullOrEmpty(winner)) + { + throw new ArgumentException($"{nameof(winner)} cannot be null or empty", nameof(winner)); + } + + _state.Winner = winner; + + return this; + } + + public EventResultBuilder SetCommenceTime(DateTime? commenceTime) + { + if (commenceTime is null) + { + throw new ArgumentNullException(nameof(commenceTime)); + } + + _state.CommenceTime = commenceTime; + + return this; + } + + public EventResultBuilder SetTimestamp(DateTime? timestamp) + { + if (timestamp is null) + { + throw new ArgumentNullException(nameof(timestamp)); + } + + _state.Timestamp = timestamp; + + return this; + } + + public EventResultBuilder SetTraceId(Guid? traceId) + { + if (traceId is null) + { + throw new ArgumentNullException(nameof(traceId)); + } + + _state.TraceId = traceId; + + return this; + } + + public EventResult Instance => _state; +} diff --git a/OddsCollector.Common/Models/Odd.cs b/OddsCollector.Common/Models/Odd.cs index f12e03b..60322c1 100644 --- a/OddsCollector.Common/Models/Odd.cs +++ b/OddsCollector.Common/Models/Odd.cs @@ -2,8 +2,8 @@ public class Odd { - public double? Away { get; init; } - public string? Bookmaker { get; init; } - public double? Draw { get; init; } - public double? Home { get; init; } + public double? Away { get; set; } + public string? Bookmaker { get; set; } + public double? Draw { get; set; } + public double? Home { get; set; } } diff --git a/OddsCollector.Common/Models/OddBuilder.cs b/OddsCollector.Common/Models/OddBuilder.cs new file mode 100644 index 0000000..87f87ba --- /dev/null +++ b/OddsCollector.Common/Models/OddBuilder.cs @@ -0,0 +1,56 @@ +namespace OddsCollector.Common.Models; + +public class OddBuilder +{ + private readonly Odd _state = new(); + + public OddBuilder SetBookmaker(string? bookmaker) + { + if (string.IsNullOrEmpty(bookmaker)) + { + throw new ArgumentException($"{nameof(bookmaker)} cannot be null or empty", nameof(bookmaker)); + } + + _state.Bookmaker = bookmaker; + + return this; + } + + public OddBuilder SetAway(double? away) + { + if (away is null) + { + throw new ArgumentNullException(nameof(away)); + } + + _state.Away = away; + + return this; + } + + public OddBuilder SetDraw(double? draw) + { + if (draw is null) + { + throw new ArgumentNullException(nameof(draw)); + } + + _state.Draw = draw; + + return this; + } + + public OddBuilder SetHome(double? home) + { + if (home is null) + { + throw new ArgumentNullException(nameof(home)); + } + + _state.Home = home; + + return this; + } + + public Odd Instance => _state; +} diff --git a/OddsCollector.Common/Models/UpcomingEvent.cs b/OddsCollector.Common/Models/UpcomingEvent.cs index 5dba2fe..9ca2888 100644 --- a/OddsCollector.Common/Models/UpcomingEvent.cs +++ b/OddsCollector.Common/Models/UpcomingEvent.cs @@ -2,11 +2,11 @@ public class UpcomingEvent { - public string? AwayTeam { get; init; } - public DateTime? CommenceTime { get; init; } - public string? HomeTeam { get; init; } - public string? Id { get; init; } - public IEnumerable? Odds { get; init; } - public DateTime? Timestamp { get; init; } - public Guid? TraceId { get; init; } + public string? AwayTeam { get; set; } + public DateTime? CommenceTime { get; set; } + public string? HomeTeam { get; set; } + public string? Id { get; set; } + public IEnumerable? Odds { get; set; } + public DateTime? Timestamp { get; set; } + public Guid? TraceId { get; set; } } diff --git a/OddsCollector.Common/Models/UpcomingEventBuilder.cs b/OddsCollector.Common/Models/UpcomingEventBuilder.cs new file mode 100644 index 0000000..ea0f6eb --- /dev/null +++ b/OddsCollector.Common/Models/UpcomingEventBuilder.cs @@ -0,0 +1,92 @@ +namespace OddsCollector.Common.Models; + +public class UpcomingEventBuilder +{ + private readonly UpcomingEvent _state = new(); + + public UpcomingEventBuilder SetAwayTeam(string? awayTeam) + { + if (string.IsNullOrEmpty(awayTeam)) + { + throw new ArgumentException($"{nameof(awayTeam)} cannot be null or empty", nameof(awayTeam)); + } + + _state.AwayTeam = awayTeam; + + return this; + } + + public UpcomingEventBuilder SetCommenceTime(DateTime? commenceTime) + { + if (commenceTime is null) + { + throw new ArgumentNullException(nameof(commenceTime)); + } + + _state.CommenceTime = commenceTime; + + return this; + } + + public UpcomingEventBuilder SetHomeTeam(string? homeTeam) + { + if (string.IsNullOrEmpty(homeTeam)) + { + throw new ArgumentException($"{nameof(homeTeam)} cannot be null or empty", nameof(homeTeam)); + } + + _state.HomeTeam = homeTeam; + + return this; + } + + public UpcomingEventBuilder SetId(string? id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentException($"{nameof(id)} cannot be null or empty", nameof(id)); + } + + _state.Id = id; + + return this; + } + + public UpcomingEventBuilder SetTimestamp(DateTime? timestamp) + { + if (timestamp is null) + { + throw new ArgumentNullException(nameof(timestamp)); + } + + _state.Timestamp = timestamp; + + return this; + } + + public UpcomingEventBuilder SetTraceId(Guid? traceId) + { + if (traceId is null) + { + throw new ArgumentNullException(nameof(traceId)); + } + + _state.TraceId = traceId; + + return this; + } + + public UpcomingEventBuilder SetOdds(IEnumerable? odds) + { + if (odds is null) + { + throw new ArgumentNullException(nameof(odds)); + } + + _state.Odds = odds; + + return this; + } + + public UpcomingEvent Instance => _state; +} diff --git a/OddsCollector.Common/OddsApi/Configuration/OddsApiOptionsFactory.cs b/OddsCollector.Common/OddsApi/Configuration/OddsApiOptionsFactory.cs new file mode 100644 index 0000000..f876467 --- /dev/null +++ b/OddsCollector.Common/OddsApi/Configuration/OddsApiOptionsFactory.cs @@ -0,0 +1,17 @@ +namespace OddsCollector.Common.OddsApi.Configuration; + +public static class OddsApiOptionsFactory +{ + public static OddsApiOptions CreateOddsApiOptions(string? leagues) + { + if (string.IsNullOrEmpty(leagues)) + { + throw new ArgumentException($"{nameof(leagues)} cannot be null or empty", nameof(leagues)); + } + + return new OddsApiOptions + { + Leagues = leagues.Split(";").ToHashSet() + }; + } +} diff --git a/OddsCollector.Common/OddsApi/Converter/OddsApiObjectConverter.cs b/OddsCollector.Common/OddsApi/Converter/OddsApiObjectConverter.cs index 7843c18..e354da1 100644 --- a/OddsCollector.Common/OddsApi/Converter/OddsApiObjectConverter.cs +++ b/OddsCollector.Common/OddsApi/Converter/OddsApiObjectConverter.cs @@ -8,8 +8,7 @@ public class OddsApiObjectConverter : IOddsApiObjectConverter { private const Markets2Key HeadToHeadMarketKey = Markets2Key.H2h; - public IEnumerable ToUpcomingEvents(ICollection? events, Guid traceId, - DateTime timestamp) + public IEnumerable ToUpcomingEvents(ICollection? events, Guid traceId, DateTime timestamp) { if (events is null) { @@ -19,8 +18,7 @@ public IEnumerable ToUpcomingEvents(ICollection? even return events.Select(e => ToUpcomingEvent(e, traceId, timestamp)); } - public IEnumerable ToEventResults(ICollection? events, Guid traceId, - DateTime timestamp) + public IEnumerable ToEventResults(ICollection? events, Guid traceId, DateTime timestamp) { if (events is null) { @@ -39,48 +37,52 @@ private static UpcomingEvent ToUpcomingEvent(Anonymous2? upcomingEvent, Guid tra throw new ArgumentNullException(nameof(upcomingEvent)); } - return new UpcomingEvent - { - AwayTeam = upcomingEvent.Away_team, - HomeTeam = upcomingEvent.Home_team, - Id = upcomingEvent.Id, - CommenceTime = upcomingEvent.Commence_time, - Timestamp = timestamp, - TraceId = traceId, - Odds = ToOdds(upcomingEvent.Bookmakers, upcomingEvent.Away_team, upcomingEvent.Home_team).ToList() - }; + return new UpcomingEventBuilder() + .SetAwayTeam(upcomingEvent.Away_team) + .SetHomeTeam(upcomingEvent.Home_team) + .SetId(upcomingEvent.Id) + .SetCommenceTime(upcomingEvent.Commence_time) + .SetTimestamp(timestamp) + .SetTraceId(traceId) + .SetOdds( + ToOdds(upcomingEvent.Bookmakers, upcomingEvent.Away_team!, upcomingEvent.Home_team!).ToList() + ) + .Instance; } - private static IEnumerable ToOdds(ICollection? bookmakers, string? awayTeam, string? homeTeam) + private static IEnumerable ToOdds(ICollection? bookmakers, string awayTeam, string homeTeam) { if (bookmakers is null) { throw new ArgumentNullException(nameof(bookmakers)); } - if (string.IsNullOrEmpty(awayTeam)) - { - throw new ArgumentException($"{nameof(awayTeam)} is null or empty", nameof(awayTeam)); - } + return bookmakers.Select(b => ToOdd(b, awayTeam, homeTeam)); + } - if (string.IsNullOrEmpty(homeTeam)) + private static Odd ToOdd(Bookmakers bookmakers, string awayTeam, string homeTeam) + { + if (bookmakers is null) { - throw new ArgumentException($"{nameof(homeTeam)} is null or empty", nameof(homeTeam)); + throw new ArgumentNullException(nameof(bookmakers)); } - return bookmakers.Select(b => ToOdd(b, awayTeam, homeTeam)); + return ToOdd(bookmakers.Markets, bookmakers.Key, awayTeam, homeTeam); } - private static Odd ToOdd(Bookmakers bookmaker, string awayTeam, string homeTeam) + private static Odd ToOdd(ICollection? markets, string? bookmaker, string awayTeam, string homeTeam) { - if (bookmaker is null) + if (markets is null) { - throw new ArgumentNullException(nameof(bookmaker)); + throw new ArgumentNullException(nameof(markets)); } - var markets = bookmaker.Markets?.FirstOrDefault(m => m.Key == HeadToHeadMarketKey); + if (!markets.Any()) + { + throw new ArgumentException($"{nameof(markets)} cannot be empty", nameof(markets)); + } - return ToOdd(markets, bookmaker.Key, awayTeam, homeTeam); + return ToOdd(markets.FirstOrDefault(m => m.Key == HeadToHeadMarketKey), bookmaker, awayTeam, homeTeam); } private static Odd ToOdd(Markets2? markets, string? bookmaker, string awayTeam, string homeTeam) @@ -90,31 +92,24 @@ private static Odd ToOdd(Markets2? markets, string? bookmaker, string awayTeam, throw new ArgumentNullException(nameof(markets)); } - if (string.IsNullOrEmpty(bookmaker)) - { - throw new ArgumentException($"{nameof(bookmaker)} is null or empty", nameof(bookmaker)); - } - return ToOdd(markets.Outcomes, bookmaker, awayTeam, homeTeam); } - private static Odd ToOdd(ICollection? outcomes, string bookmaker, string awayTeam, string homeTeam) + private static Odd ToOdd(ICollection? outcomes, string? bookmaker, string awayTeam, string homeTeam) { if (outcomes is null) { throw new ArgumentNullException(nameof(outcomes)); } - return new Odd - { - Bookmaker = bookmaker, - Home = GetOdd(outcomes, homeTeam), - Away = GetOdd(outcomes, awayTeam), - Draw = GetOdd(outcomes, Constants.Draw) - }; + return new OddBuilder() + .SetBookmaker(bookmaker) + .SetHome(GetScore(outcomes, homeTeam)) + .SetAway(GetScore(outcomes, awayTeam)) + .SetDraw(GetScore(outcomes, Constants.Draw)).Instance; } - private static double? GetOdd(IEnumerable outcomes, string oddType) + private static double? GetScore(IEnumerable outcomes, string oddType) { if (outcomes is null) { @@ -133,20 +128,18 @@ private static Odd ToOdd(ICollection? outcomes, string bookmaker, strin throw new ArgumentException($"{nameof(outcomes)} has duplicates for {oddType}", nameof(outcomes)); } - return matches.First().Price ?? - throw new ArgumentException($"{nameof(outcomes)} doesn't have price for {oddType}", nameof(outcomes)); + return matches.First().Price; } private static EventResult ToEventResult(Anonymous3 eventResult, Guid traceId, DateTime timestamp) { - return new EventResult - { - Id = eventResult.Id, - CommenceTime = eventResult.Commence_time, - Timestamp = timestamp, - TraceId = traceId, - Winner = GetWinner(eventResult.Scores, eventResult.Away_team, eventResult.Home_team) - }; + return new EventResultBuilder() + .SetId(eventResult.Id) + .SetCommenceTime(eventResult.Commence_time) + .SetTimestamp(timestamp) + .SetTraceId(traceId) + .SetWinner(GetWinner(eventResult.Scores, eventResult.Away_team, eventResult.Home_team)) + .Instance; } private static string GetWinner(ICollection? scores, string? awayTeam, string? homeTeam) @@ -156,6 +149,11 @@ private static string GetWinner(ICollection? scores, string? awayTea throw new ArgumentNullException(nameof(scores)); } + if (scores.Count < 2) + { + throw new ArgumentException($"{nameof(scores)} must have at least 2 elements", nameof(scores)); + } + if (string.IsNullOrEmpty(awayTeam)) { throw new ArgumentException($"{nameof(awayTeam)} is null or empty", nameof(awayTeam)); diff --git a/OddsCollector.Functions.EventResults/EventResultsFunction.cs b/OddsCollector.Functions.EventResults/EventResultsFunction.cs index 80c1582..562ae74 100644 --- a/OddsCollector.Functions.EventResults/EventResultsFunction.cs +++ b/OddsCollector.Functions.EventResults/EventResultsFunction.cs @@ -6,6 +6,8 @@ using OddsCollector.Common.OddsApi.Configuration; [assembly: InternalsVisibleTo("OddsCollector.Functions.EventResults.Tests")] +// DynamicProxyGenAssembly2 is a temporary assembly built by mocking systems that use CastleProxy +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] namespace OddsCollector.Functions.EventResults; diff --git a/OddsCollector.Functions.EventResults/Program.cs b/OddsCollector.Functions.EventResults/Program.cs index a4f3603..cf93216 100644 --- a/OddsCollector.Functions.EventResults/Program.cs +++ b/OddsCollector.Functions.EventResults/Program.cs @@ -12,16 +12,15 @@ .ConfigureFunctionsWorkerDefaults() .ConfigureServices(services => { - // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 - services.Configure(o => - { - o.Leagues = Environment.GetEnvironmentVariable("OddsApi:Leagues")!.Split(";").ToHashSet(); - }); + services.Configure( + // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 + _ => OddsApiOptionsFactory.CreateOddsApiOptions(Environment.GetEnvironmentVariable("OddsApi:Leagues")) + ); services.AddHttpClient(); services.AddSingleton(); services.AddSingleton(); - // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 services.AddSingleton( + // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 SecretClientFactory.CreateSecretClient(Environment.GetEnvironmentVariable("KeyVault:Name")) ); services.AddSingleton(); diff --git a/OddsCollector.Functions.Notification/CosmosDb/CosmosDbClient.cs b/OddsCollector.Functions.Notification/CosmosDb/CosmosDbClient.cs index 1c3d65a..584fa90 100644 --- a/OddsCollector.Functions.Notification/CosmosDb/CosmosDbClient.cs +++ b/OddsCollector.Functions.Notification/CosmosDb/CosmosDbClient.cs @@ -22,8 +22,8 @@ public CosmosDbClient(IOptions options) // cosmosdb doesn't support grouping var matches = from prediction in queryable - where prediction.CommenceTime >= DateTime.UtcNow - select prediction; + where prediction.CommenceTime >= DateTime.UtcNow + select prediction; var cosmosdbresult = new List(); diff --git a/OddsCollector.Functions.Predictions.Tests/GlobalUsings.cs b/OddsCollector.Functions.Predictions.Tests/GlobalUsings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/OddsCollector.Functions.Predictions.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/OddsCollector.Functions.Predictions.Tests/OddsCollector.Functions.Predictions.Tests.csproj b/OddsCollector.Functions.Predictions.Tests/OddsCollector.Functions.Predictions.Tests.csproj new file mode 100644 index 0000000..5332639 --- /dev/null +++ b/OddsCollector.Functions.Predictions.Tests/OddsCollector.Functions.Predictions.Tests.csproj @@ -0,0 +1,32 @@ + + + + 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.Predictions.Tests/PredictionFunctionTests.cs b/OddsCollector.Functions.Predictions.Tests/PredictionFunctionTests.cs new file mode 100644 index 0000000..013a1e8 --- /dev/null +++ b/OddsCollector.Functions.Predictions.Tests/PredictionFunctionTests.cs @@ -0,0 +1,94 @@ +using System.Text; +using System.Text.Json; +using Azure.Core.Amqp; +using Azure.Messaging.ServiceBus; +using FluentAssertions; +using Microsoft.Azure.Functions.Worker; +using NSubstitute; +using NSubstitute.ReceivedExtensions; +using OddsCollector.Common.Models; +using OddsCollector.Functions.Predictions.Strategies; + +namespace OddsCollector.Functions.Predictions.Tests; + +internal class PredictionFunctionTests +{ + [Test] + public void Constructor_WithValidDependencies_ReturnsNewInstance() + { + var stub = Substitute.For(); + + var function = new PredictionFunction(stub); + + function.Should().NotBeNull(); + } + + [Test] + public void Constructor_WithNullStrategy_ThrowException() + { + var action = () => + { + _ = new PredictionFunction(null); + }; + + action.Should().Throw().WithParameterName("strategy"); + } + + [Test] + public async Task Run_WithServiceBusMessage_ReturnsEventPrediction() + { + var upcomingEvent = new UpcomingEvent() + { + AwayTeam = "Liverpool", + CommenceTime = new DateTime(2023, 11, 25, 12, 30, 0), + HomeTeam = "Manchester City", + Id = "4acd8f2675ca847ba33eea3664f6c0bb", + TraceId = Guid.NewGuid(), + Timestamp = DateTime.Now, + Odds = new List() + { + new() + { + Away = 4.08, + Bookmaker = "betclic", + Draw = 3.82, + Home = 1.7 + } + } + }; + + var expectedPrediction = new EventPrediction() + { + AwayTeam = "Liverpool", + Bookmaker = "betclic", + CommenceTime = new DateTime(2023, 11, 25, 12, 30, 0), + HomeTeam = "Manchester City", + Id = "4acd8f2675ca847ba33eea3664f6c0bb", + Strategy = nameof(AdjustedConsensusStrategy), + Timestamp = DateTime.Now, + TraceId = Guid.NewGuid(), + Winner = "Manchester City" + }; + + var serialized = Encoding.ASCII.GetBytes(JsonSerializer.Serialize(upcomingEvent)).Select(x => new ReadOnlyMemory(new byte[] { x })); + + var ampqmessage = new AmqpMessageBody(serialized); + + var ampqannotatedmessage = new AmqpAnnotatedMessage(ampqmessage); + + var receivedmessage = ServiceBusReceivedMessage.FromAmqpMessage(ampqannotatedmessage, new BinaryData(Array.Empty())); + + var actionsMock = Substitute.For(); + + var strategyStub = Substitute.For(); + strategyStub.GetPrediction(Arg.Any(), Arg.Any()).Returns(expectedPrediction); + + var function = new PredictionFunction(strategyStub); + + var prediction = await function.Run(receivedmessage, actionsMock).ConfigureAwait(false); + + prediction.Should().NotBeNull().And.Be(expectedPrediction); + + await actionsMock.Received(Quantity.Exactly(1)).CompleteMessageAsync(Arg.Any()); + } +} diff --git a/OddsCollector.Functions.Predictions.Tests/Strategies/AdjustedConsensusStrategyTests.cs b/OddsCollector.Functions.Predictions.Tests/Strategies/AdjustedConsensusStrategyTests.cs new file mode 100644 index 0000000..08965c5 --- /dev/null +++ b/OddsCollector.Functions.Predictions.Tests/Strategies/AdjustedConsensusStrategyTests.cs @@ -0,0 +1,374 @@ +using FluentAssertions; +using OddsCollector.Common.Models; +using OddsCollector.Functions.Predictions.Strategies; + +namespace OddsCollector.Functions.Predictions.Tests.Strategies; + +internal class AdjustedConsensusStrategyTests +{ + [Test] + public void GetPrediction_WithUpcomingEventWithWinningHomeTeam_ReturnsPrediction() + { + var upcomingEvent = new UpcomingEventBuilder().SetDefaults().GetUpcomingEvent(); + + var timestamp = DateTime.Now; + + var strategy = new AdjustedConsensusStrategy(); + + var prediction = strategy.GetPrediction(upcomingEvent, timestamp); + + prediction.Should().NotBeNull(); + prediction.AwayTeam.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultAwayTeam); + prediction.Bookmaker.Should().NotBeNull().And.Be("betclic"); + prediction.CommenceTime.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultCommenceTime); + prediction.HomeTeam.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultHomeTeam); + prediction.Id.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultId); + prediction.Strategy.Should().NotBeNull().And.Be(nameof(AdjustedConsensusStrategy)); + prediction.Timestamp.Should().NotBeNull().And.Be(timestamp); + prediction.TraceId.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultTraceId); + prediction.Winner.Should().NotBeNull().And.Be("Manchester City"); + } + + [Test] + public void GetPrediction_WithUpcomingEventWithWinningAwayTeam_ReturnsPrediction() + { + var upcomingEvent = new UpcomingEventBuilder().SetDefaults().SetOdds(new List + { + new() + { + Away = 1.8, + Bookmaker = "betclic", + Draw = 3.82, + Home = 4.08 + }, + new() + { + Away = 1.7, + Bookmaker = "sport888", + Draw = 4.33, + Home = 4.33 + }, + new() + { + Away = 1.67, + Bookmaker = "mybookieag", + Draw = 4.5, + Home = 4.5 + } + }).GetUpcomingEvent(); + + var timestamp = DateTime.Now; + + var strategy = new AdjustedConsensusStrategy(); + + var prediction = strategy.GetPrediction(upcomingEvent, timestamp); + + prediction.Should().NotBeNull(); + prediction.AwayTeam.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultAwayTeam); + prediction.Bookmaker.Should().NotBeNull().And.Be("betclic"); + prediction.CommenceTime.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultCommenceTime); + prediction.HomeTeam.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultHomeTeam); + prediction.Id.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultId); + prediction.Strategy.Should().NotBeNull().And.Be(nameof(AdjustedConsensusStrategy)); + prediction.Timestamp.Should().NotBeNull().And.Be(timestamp); + prediction.TraceId.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultTraceId); + prediction.Winner.Should().NotBeNull().And.Be("Liverpool"); + } + + [Test] + public void GetPrediction_WithUpcomingEventWithDraw_ReturnsPrediction() + { + var upcomingEvent = new UpcomingEventBuilder().SetDefaults().SetOdds(new List + { + new() + { + Away = 3.82, + Bookmaker = "betclic", + Draw = 1.8, + Home = 4.08 + }, + new() + { + Away = 4.33, + Bookmaker = "sport888", + Draw = 1.7, + Home = 4.33 + }, + new() + { + Away = 4.5, + Bookmaker = "mybookieag", + Draw = 1.67, + Home = 4.5 + } + }).GetUpcomingEvent(); + + var timestamp = DateTime.Now; + + var strategy = new AdjustedConsensusStrategy(); + + var prediction = strategy.GetPrediction(upcomingEvent, timestamp); + + prediction.Should().NotBeNull(); + prediction.AwayTeam.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultAwayTeam); + prediction.Bookmaker.Should().NotBeNull().And.Be("betclic"); + prediction.CommenceTime.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultCommenceTime); + prediction.HomeTeam.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultHomeTeam); + prediction.Id.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultId); + prediction.Strategy.Should().NotBeNull().And.Be(nameof(AdjustedConsensusStrategy)); + prediction.Timestamp.Should().NotBeNull().And.Be(timestamp); + prediction.TraceId.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultTraceId); + prediction.Winner.Should().NotBeNull().And.Be("Draw"); + } + + [Test] + public void GetPrediction_WithValidUpcomingEvent_ReturnsPrediction() + { + var upcomingEvent = new UpcomingEventBuilder().SetDefaults().GetUpcomingEvent(); + + var timestamp = DateTime.Now; + + var strategy = new AdjustedConsensusStrategy(); + + var prediction = strategy.GetPrediction(upcomingEvent, timestamp); + + prediction.Should().NotBeNull(); + prediction.AwayTeam.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultAwayTeam); + prediction.Bookmaker.Should().NotBeNull().And.Be("betclic"); + prediction.CommenceTime.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultCommenceTime); + prediction.HomeTeam.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultHomeTeam); + prediction.Id.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultId); + prediction.Strategy.Should().NotBeNull().And.Be(nameof(AdjustedConsensusStrategy)); + prediction.Timestamp.Should().NotBeNull().And.Be(timestamp); + prediction.TraceId.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultTraceId); + prediction.Winner.Should().NotBeNull().And.Be("Manchester City"); + } + + [Test] + public void GetPrediction_WithNullUpcomingEvent_ThrowsException() + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(null, DateTime.Now); + }; + + action.Should().Throw().WithParameterName("upcomingEvent"); + } + + [Test] + public void GetPrediction_WithNullTimestamp_ThrowsException() + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().GetUpcomingEvent(), null); + }; + + action.Should().Throw().WithParameterName("timestamp"); + } + + [TestCase("")] + [TestCase(null)] + public void GetPrediction_WithNullOrEmptyId_ThrowsException(string? id) + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().SetId(id).GetUpcomingEvent(), DateTime.Now); + }; + + action.Should().Throw().WithParameterName(nameof(id)); + } + + [Test] + public void GetPrediction_WithNullEventTimestamp_ReturnsNewInstance() + { + var upcomingEvent = new UpcomingEventBuilder().SetDefaults().SetTimestamp(null).GetUpcomingEvent(); + + var timestamp = DateTime.Now; + + var strategy = new AdjustedConsensusStrategy(); + + var prediction = strategy.GetPrediction(upcomingEvent, timestamp); + + prediction.Should().NotBeNull(); + prediction.AwayTeam.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultAwayTeam); + prediction.Bookmaker.Should().NotBeNull().And.Be("betclic"); + prediction.CommenceTime.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultCommenceTime); + prediction.HomeTeam.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultHomeTeam); + prediction.Id.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultId); + prediction.Strategy.Should().NotBeNull().And.Be(nameof(AdjustedConsensusStrategy)); + prediction.Timestamp.Should().NotBeNull().And.Be(timestamp); + prediction.TraceId.Should().NotBeNull().And.Be(UpcomingEventBuilder.DefaultTraceId); + prediction.Winner.Should().NotBeNull().And.Be("Manchester City"); + } + + [Test] + public void GetPrediction_WithNullEventTraceId_ThrowsException() + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().SetTraceId(null).GetUpcomingEvent(), DateTime.Now); + }; + + action.Should().Throw().WithParameterName("traceId"); + } + + [Test] + public void GetPrediction_WithNullEventCommenceTime_ThrowsException() + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().SetCommenceTime(null).GetUpcomingEvent(), DateTime.Now); + }; + + action.Should().Throw().WithParameterName("commenceTime"); + } + + [TestCase("")] + [TestCase(null)] + public void GetPrediction_WithNullEventAwayTeam_ThrowsException(string? awayTeam) + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().SetAwayTeam(null).GetUpcomingEvent(), DateTime.Now); + }; + + action.Should().Throw().WithParameterName(nameof(awayTeam)); + } + + [TestCase("")] + [TestCase(null)] + public void GetPrediction_WithNullEventHomeTeam_ThrowsException(string? homeTeam) + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().SetHomeTeam(null).GetUpcomingEvent(), DateTime.Now); + }; + + action.Should().Throw().WithParameterName(nameof(homeTeam)); + } + + [Test] + public void GetPrediction_WithNullOdds_ThrowsException() + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().SetOdds(null).GetUpcomingEvent(), DateTime.Now); + }; + + action.Should().Throw().WithParameterName("odds"); + } + + [Test] + public void GetPrediction_WithEmptyOdds_ThrowsException() + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().SetOdds(new List() { }).GetUpcomingEvent(), DateTime.Now); + }; + + action.Should().Throw().WithParameterName("odds"); + } + + [Test] + public void GetPrediction_WithNullAwayScoreInOdds_ThrowsException() + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().SetOdds(new List() { + new() + { + Away = null, + Bookmaker = "betclic", + Draw = 3.82, + Home = 1.8 + } + }).GetUpcomingEvent(), DateTime.Now); + }; + + action.Should().Throw().WithParameterName("value"); + } + + [TestCase("")] + [TestCase(null)] + public void GetPrediction_WithNullOrEmptyBookmakerInOdds_ThrowsException(string? bookmaker) + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().SetOdds(new List() { + new() + { + Away = 4.08, + Bookmaker = bookmaker, + Draw = 3.82, + Home = 1.8 + } + }).GetUpcomingEvent(), DateTime.Now); + }; + + action.Should().Throw().WithParameterName(nameof(bookmaker)); + } + + [Test] + public void GetPrediction_WithNullDrawScoreInOdds_ThrowsException() + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().SetOdds(new List() { + new() + { + Away = 4.08, + Bookmaker = "betclic", + Draw = null, + Home = 1.8 + } + }).GetUpcomingEvent(), DateTime.Now); + }; + + action.Should().Throw().WithParameterName("value"); + } + + [Test] + public void GetPrediction_WithNullHomeScoreInOdds_ThrowsException() + { + var strategy = new AdjustedConsensusStrategy(); + + var action = () => + { + _ = strategy.GetPrediction(new UpcomingEventBuilder().SetDefaults().SetOdds(new List() { + new() + { + Away = 4.08, + Bookmaker = "betclic", + Draw = 3.82, + Home = null + } + }).GetUpcomingEvent(), DateTime.Now); + }; + + action.Should().Throw().WithParameterName("value"); + } +} diff --git a/OddsCollector.Functions.Predictions.Tests/Strategies/UpcomingEventBuilder.cs b/OddsCollector.Functions.Predictions.Tests/Strategies/UpcomingEventBuilder.cs new file mode 100644 index 0000000..c32e7ee --- /dev/null +++ b/OddsCollector.Functions.Predictions.Tests/Strategies/UpcomingEventBuilder.cs @@ -0,0 +1,109 @@ +using OddsCollector.Common.Models; + +namespace OddsCollector.Functions.Predictions.Tests.Strategies; + +internal class UpcomingEventBuilder +{ + public const string DefaultAwayTeam = "Liverpool"; + public const string DefaultHomeTeam = "Manchester City"; + public const string DefaultId = "4acd8f2675ca847ba33eea3664f6c0bb"; + public static readonly DateTime DefaultCommenceTime = new(2023, 11, 25, 12, 30, 0); + public static readonly DateTime DefaultTimestamp = new(2023, 11, 25, 15, 30, 0); + public static readonly Guid DefaultTraceId = new("447b57dd-84bc-4e79-95d0-695f7493bf41"); + public static readonly IEnumerable DefaultOdds = new List + { + new() + { + Away = 4.08, + Bookmaker = "betclic", + Draw = 3.82, + Home = 1.8 + }, + new() + { + Away = 4.33, + Bookmaker = "sport888", + Draw = 4.33, + Home = 1.7 + }, + new() + { + Away = 4.5, + Bookmaker = "mybookieag", + Draw = 4.5, + Home = 1.67 + } + }; + + private UpcomingEvent _state = new(); + + public UpcomingEventBuilder SetDefaults() + { + _state = new UpcomingEvent + { + AwayTeam = DefaultAwayTeam, + CommenceTime = DefaultCommenceTime, + HomeTeam = DefaultHomeTeam, + Id = DefaultId, + Timestamp = DefaultTimestamp, + TraceId = DefaultTraceId, + Odds = DefaultOdds + }; + + return this; + } + + public UpcomingEventBuilder SetAwayTeam(string? awayTeam) + { + _state.AwayTeam = awayTeam; + + return this; + } + + public UpcomingEventBuilder SetCommenceTime(DateTime? commenceTime) + { + _state.CommenceTime = commenceTime; + + return this; + } + + public UpcomingEventBuilder SetHomeTeam(string? homeTeam) + { + _state.HomeTeam = homeTeam; + + return this; + } + + public UpcomingEventBuilder SetId(string? id) + { + _state.Id = id; + + return this; + } + + public UpcomingEventBuilder SetTimestamp(DateTime? timestamp) + { + _state.Timestamp = timestamp; + + return this; + } + + public UpcomingEventBuilder SetTraceId(Guid? traceId) + { + _state.TraceId = traceId; + + return this; + } + + public UpcomingEventBuilder SetOdds(IEnumerable? odds) + { + _state.Odds = odds; + + return this; + } + + public UpcomingEvent GetUpcomingEvent() + { + return _state; + } +} diff --git a/OddsCollector.Functions.Predictions/OddsCollector.Functions.Predictions.csproj b/OddsCollector.Functions.Predictions/OddsCollector.Functions.Predictions.csproj index 4ac3bb3..1c9e9fe 100644 --- a/OddsCollector.Functions.Predictions/OddsCollector.Functions.Predictions.csproj +++ b/OddsCollector.Functions.Predictions/OddsCollector.Functions.Predictions.csproj @@ -8,15 +8,16 @@ cfaec948-c93a-429d-8cb9-3e483cca8073 - - - - - - + + + + + + + - + @@ -28,6 +29,6 @@ - + \ No newline at end of file diff --git a/OddsCollector.Functions.Predictions/PredictionFunction.cs b/OddsCollector.Functions.Predictions/PredictionFunction.cs index 9de39eb..66b3130 100644 --- a/OddsCollector.Functions.Predictions/PredictionFunction.cs +++ b/OddsCollector.Functions.Predictions/PredictionFunction.cs @@ -1,17 +1,22 @@ -using Azure.Messaging.ServiceBus; +using System.Runtime.CompilerServices; +using Azure.Messaging.ServiceBus; using Microsoft.Azure.Functions.Worker; using OddsCollector.Common.Models; using OddsCollector.Functions.Predictions.Strategies; +[assembly: InternalsVisibleTo("OddsCollector.Functions.Predictions.Tests")] +// DynamicProxyGenAssembly2 is a temporary assembly built by mocking systems that use CastleProxy +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] + namespace OddsCollector.Functions.Predictions; internal sealed class PredictionFunction { private readonly IPredictionStrategy _strategy; - public PredictionFunction(IPredictionStrategy strategy) + public PredictionFunction(IPredictionStrategy? strategy) { - _strategy = strategy; + _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); } [Function(nameof(PredictionFunction))] diff --git a/OddsCollector.Functions.Predictions/Strategies/AdjustedConsensusStrategy.cs b/OddsCollector.Functions.Predictions/Strategies/AdjustedConsensusStrategy.cs index 8b7fbbc..2924a6a 100644 --- a/OddsCollector.Functions.Predictions/Strategies/AdjustedConsensusStrategy.cs +++ b/OddsCollector.Functions.Predictions/Strategies/AdjustedConsensusStrategy.cs @@ -20,83 +20,67 @@ public EventPrediction GetPrediction(UpcomingEvent? upcomingEvent, DateTime? tim throw new ArgumentNullException(nameof(timestamp)); } - return MakePrediction(upcomingEvent, timestamp.Value); + var score = GetWinner(upcomingEvent.Odds, upcomingEvent.AwayTeam, upcomingEvent.HomeTeam); + + return new EventPredictionBuilder() + .SetAwayTeam(upcomingEvent.AwayTeam) + .SetHomeTeam(upcomingEvent.HomeTeam) + .SetCommenceTime(upcomingEvent.CommenceTime) + .SetStrategy(nameof(AdjustedConsensusStrategy)) + .SetId(upcomingEvent.Id) + .SetTimestamp(timestamp) + .SetTraceId(upcomingEvent.TraceId) + .SetWinner(score.Name) + .SetBookmaker(score.Bookmaker) + .Instance; } - private static EventPrediction MakePrediction(UpcomingEvent upcomingEvent, DateTime timestamp) - { - var winner = PredictWinner(upcomingEvent.Odds, upcomingEvent.AwayTeam, upcomingEvent.HomeTeam); - var odd = GetBestOdd(upcomingEvent.Odds, winner, upcomingEvent.AwayTeam, upcomingEvent.HomeTeam); - - return new EventPrediction - { - Id = upcomingEvent.Id, - Timestamp = timestamp, - TraceId = upcomingEvent.TraceId, - Winner = winner, - Bookmaker = odd?.Bookmaker, - Strategy = nameof(AdjustedConsensusStrategy), - CommenceTime = upcomingEvent?.CommenceTime, - AwayTeam = upcomingEvent?.AwayTeam, - HomeTeam = upcomingEvent?.HomeTeam - }; - } - - private static string PredictWinner(IEnumerable? odds, string? awayTeam, string? homeTeam) + private static StrategyScore GetWinner(IEnumerable? odds, string? awayTeam, string? homeTeam) { if (odds is null) { throw new ArgumentNullException(nameof(odds)); } + if (!odds.Any()) + { + throw new ArgumentException($"{nameof(odds)}.Odds cannot be empty", nameof(odds)); + } + if (string.IsNullOrEmpty(awayTeam)) { - throw new ArgumentException($"{nameof(awayTeam)} is null or empty", nameof(awayTeam)); + throw new ArgumentException($"{nameof(awayTeam)}.AwayTeam is null or empty", nameof(awayTeam)); } if (string.IsNullOrEmpty(homeTeam)) { - throw new ArgumentException($"{nameof(homeTeam)} is null or empty", nameof(homeTeam)); + throw new ArgumentException($"{nameof(homeTeam)}.HomeTeam is null or empty", nameof(homeTeam)); } - return CalculateScores(odds, awayTeam, homeTeam).MaxBy(p => p.Value).Key; - } + IEnumerable filteredOdds = odds.Where(o => o is not null).ToList()!; - private static Dictionary CalculateScores(IEnumerable odds, string awayTeam, string homeTeam) - { - var enumeratedOdds = odds.ToList(); - - return new Dictionary + var scores = new List() { - { Constants.Draw, CalculateAdjustedScore(enumeratedOdds, Draw, 0.057) }, - { awayTeam, CalculateAdjustedScore(enumeratedOdds, AwayTeamWins, 0.034) }, - { homeTeam, CalculateAdjustedScore(enumeratedOdds, HomeTeamWins, 0.037) } + new StrategyScoreBuilder().SetName(Constants.Draw).SetValue(CalculateAdjustedScore(filteredOdds, Draw, 0.057)).Instance, + new StrategyScoreBuilder().SetName(awayTeam).SetValue(CalculateAdjustedScore(filteredOdds, AwayTeamWins, 0.034)).Instance, + new StrategyScoreBuilder().SetName(homeTeam).SetValue(CalculateAdjustedScore(filteredOdds, HomeTeamWins, 0.037)).Instance }; + + var winner = scores.MaxBy(p => p.Value)!; + + winner.Bookmaker = GetBestOdd(odds!, winner.Name!, awayTeam, homeTeam)?.Bookmaker; + + return winner; } - private static double? CalculateAdjustedScore(IEnumerable odds, Func filter, double adjustment) + private static double? CalculateAdjustedScore(IEnumerable odds, Func filter, double adjustment) { var average = odds.Select(filter).Average(); return average == 0 ? 0 : (1 / average) - adjustment; } - private static Odd? GetBestOdd(IEnumerable? odds, string winner, string? awayTeam, string? homeTeam) + private static Odd? GetBestOdd(IEnumerable odds, string winner, string awayTeam, string homeTeam) { - if (odds is null) - { - throw new ArgumentNullException(nameof(odds)); - } - - if (string.IsNullOrEmpty(awayTeam)) - { - throw new ArgumentException($"{nameof(awayTeam)} is null or empty", nameof(awayTeam)); - } - - if (string.IsNullOrEmpty(homeTeam)) - { - throw new ArgumentException($"{nameof(homeTeam)} is null or empty", nameof(homeTeam)); - } - if (winner == Constants.Draw) { return GetBestOdd(odds, Draw); @@ -105,38 +89,23 @@ private static string PredictWinner(IEnumerable? odds, string? awayTeam, s return winner == awayTeam ? GetBestOdd(odds, AwayTeamWins) : GetBestOdd(odds, HomeTeamWins); } - private static Odd? GetBestOdd(IEnumerable odds, Func filter) + private static Odd? GetBestOdd(IEnumerable odds, Func filter) { - return odds.OrderByDescending(filter).First(); + return odds.Where(o => o is not null).OrderByDescending(filter).FirstOrDefault(); } - private static double? Draw(Odd? odd) + private static double? Draw(Odd odd) { - if (odd is null) - { - throw new ArgumentNullException(nameof(odd)); - } - return odd.Draw; } - private static double? AwayTeamWins(Odd? odd) + private static double? AwayTeamWins(Odd odd) { - if (odd is null) - { - throw new ArgumentNullException(nameof(odd)); - } - return odd.Away; } - private static double? HomeTeamWins(Odd? odd) + private static double? HomeTeamWins(Odd odd) { - if (odd is null) - { - throw new ArgumentNullException(nameof(odd)); - } - return odd.Home; } } diff --git a/OddsCollector.Functions.Predictions/Strategies/StrategyScore.cs b/OddsCollector.Functions.Predictions/Strategies/StrategyScore.cs new file mode 100644 index 0000000..ad949db --- /dev/null +++ b/OddsCollector.Functions.Predictions/Strategies/StrategyScore.cs @@ -0,0 +1,8 @@ +namespace OddsCollector.Functions.Predictions.Strategies; + +internal class StrategyScore +{ + public string? Name { get; set; } + public double? Value { get; set; } + public string? Bookmaker { get; set; } +} diff --git a/OddsCollector.Functions.Predictions/Strategies/StrategyScoreBuilder.cs b/OddsCollector.Functions.Predictions/Strategies/StrategyScoreBuilder.cs new file mode 100644 index 0000000..081c76d --- /dev/null +++ b/OddsCollector.Functions.Predictions/Strategies/StrategyScoreBuilder.cs @@ -0,0 +1,44 @@ +namespace OddsCollector.Functions.Predictions.Strategies; + +internal class StrategyScoreBuilder +{ + private readonly StrategyScore _state = new(); + + public StrategyScoreBuilder SetBookmaker(string? bookmaker) + { + if (string.IsNullOrEmpty(bookmaker)) + { + throw new ArgumentException($"{nameof(bookmaker)} cannot be null or empty", nameof(bookmaker)); + } + + _state.Bookmaker = bookmaker; + + return this; + } + + public StrategyScoreBuilder SetName(string? name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException($"{nameof(name)} cannot be null or empty", nameof(name)); + } + + _state.Name = name; + + return this; + } + + public StrategyScoreBuilder SetValue(double? value) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + _state.Value = value; + + return this; + } + + public StrategyScore Instance => _state; +} diff --git a/OddsCollector.Functions.UpcomingEvents/Program.cs b/OddsCollector.Functions.UpcomingEvents/Program.cs index e01ecb2..cf93216 100644 --- a/OddsCollector.Functions.UpcomingEvents/Program.cs +++ b/OddsCollector.Functions.UpcomingEvents/Program.cs @@ -12,16 +12,15 @@ .ConfigureFunctionsWorkerDefaults() .ConfigureServices(services => { - // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 - services.Configure(o => - { - o.Leagues = Environment.GetEnvironmentVariable("OddsApi:Leagues").Split(";").ToHashSet(); - }); + services.Configure( + // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 + _ => OddsApiOptionsFactory.CreateOddsApiOptions(Environment.GetEnvironmentVariable("OddsApi:Leagues")) + ); services.AddHttpClient(); services.AddSingleton(); services.AddSingleton(); - // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 services.AddSingleton( + // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 SecretClientFactory.CreateSecretClient(Environment.GetEnvironmentVariable("KeyVault:Name")) ); services.AddSingleton(); diff --git a/OddsCollector.Functions.UpcomingEvents/UpcomingEventsFunction.cs b/OddsCollector.Functions.UpcomingEvents/UpcomingEventsFunction.cs index 31a85e1..5f9975e 100644 --- a/OddsCollector.Functions.UpcomingEvents/UpcomingEventsFunction.cs +++ b/OddsCollector.Functions.UpcomingEvents/UpcomingEventsFunction.cs @@ -6,6 +6,8 @@ using OddsCollector.Common.OddsApi.Configuration; [assembly: InternalsVisibleTo("OddsCollector.Functions.UpcomingEvents.Tests")] +// DynamicProxyGenAssembly2 is a temporary assembly built by mocking systems that use CastleProxy +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] namespace OddsCollector.Functions.UpcomingEvents; diff --git a/OddsCollector.sln b/OddsCollector.sln index 9c99ce4..5ee5384 100644 --- a/OddsCollector.sln +++ b/OddsCollector.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OddsCollector.Functions.Eve 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}" 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}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,10 @@ Global {D2E01FC2-A174-40FC-BC56-6EE3FD640AE1}.Debug|Any CPU.Build.0 = Debug|Any CPU {D2E01FC2-A174-40FC-BC56-6EE3FD640AE1}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2E01FC2-A174-40FC-BC56-6EE3FD640AE1}.Release|Any CPU.Build.0 = Release|Any CPU + {296C1955-19D3-49E5-A4E5-E68424525E8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE