From 8876f77cef58858633da0cb4648d34b66477e6b8 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 11:33:31 +0000 Subject: [PATCH 01/35] Minor: Added .vs to gitignore, updated ExchangeRateUpdater to .net8.0 --- .gitignore | 1 + jobs/Backend/Task/ExchangeRateUpdater.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fd3586545..f54dfa297 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ node_modules bower_components npm-debug.log +.vs \ No newline at end of file diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 2fc654a12..2215fc8e9 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 \ No newline at end of file From 0d84379e92042bb64fa70a26efc131a3a82a8360 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 11:35:58 +0000 Subject: [PATCH 02/35] Minor: Added test project to ExchangeRateUpdater --- .../GlobalUsings.cs | 1 + .../TestExchangeRateUpdater.Tests.csproj | 20 +++++++++++++++++++ .../UnitTest1.cs | 16 +++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 jobs/Backend/TestExchangeRateUpdater.Tests/GlobalUsings.cs create mode 100644 jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj create mode 100644 jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/GlobalUsings.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/GlobalUsings.cs new file mode 100644 index 000000000..cefced496 --- /dev/null +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj b/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj new file mode 100644 index 000000000..fa8b864cf --- /dev/null +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs new file mode 100644 index 000000000..c3479a47c --- /dev/null +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs @@ -0,0 +1,16 @@ +namespace TestExchangeRateUpdater.Tests +{ + public class Tests + { + [SetUp] + public void Setup() + { + } + + [Test] + public void Test1() + { + Assert.Pass(); + } + } +} \ No newline at end of file From f2a02abdf66071a998ad6bc4a477d477f45739d3 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 11:39:22 +0000 Subject: [PATCH 03/35] Minor: Updated ExchangeRateUpdater test project packages to latest stable --- jobs/Backend/Task/ExchangeRateUpdater.sln | 53 +++++++++++++---------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/jobs/Backend/Task/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater.sln index 89be84daf..9f4d88626 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.sln +++ b/jobs/Backend/Task/ExchangeRateUpdater.sln @@ -1,22 +1,31 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34309.116 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestExchangeRateUpdater.Tests", "..\TestExchangeRateUpdater.Tests\TestExchangeRateUpdater.Tests.csproj", "{D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.Build.0 = Release|Any CPU + {D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {26DF4BBA-AE0E-4E97-B657-B6406E4C09F2} + EndGlobalSection +EndGlobal From 967fa28098f9d6258769c9cfdb1da09c7e670922 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 11:40:01 +0000 Subject: [PATCH 04/35] Minor: Updated ExchangeRateUpdater test project packages to latest stable --- .../TestExchangeRateUpdater.Tests.csproj | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj b/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj index fa8b864cf..57e362a4f 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj @@ -10,11 +10,17 @@ - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From c129ae057294a152d6f084b21103d856e4d5ada2 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 12:49:13 +0000 Subject: [PATCH 05/35] Minor: Converted ExchangeRateUpdater to file scoped namespacing --- jobs/Backend/Task/Currency.cs | 27 +++++---- jobs/Backend/Task/ExchangeRate.cs | 29 +++++----- jobs/Backend/Task/ExchangeRateProvider.cs | 23 ++++---- jobs/Backend/Task/Program.cs | 57 +++++++++---------- .../UnitTest1.cs | 21 ++++--- 5 files changed, 76 insertions(+), 81 deletions(-) diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/Currency.cs index f375776f2..057eb8183 100644 --- a/jobs/Backend/Task/Currency.cs +++ b/jobs/Backend/Task/Currency.cs @@ -1,20 +1,19 @@ -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater; + +public class Currency { - public class Currency + public Currency(string code) { - public Currency(string code) - { - Code = code; - } + Code = code; + } - /// - /// Three-letter ISO 4217 code of the currency. - /// - public string Code { get; } + /// + /// Three-letter ISO 4217 code of the currency. + /// + public string Code { get; } - public override string ToString() - { - return Code; - } + public override string ToString() + { + return Code; } } diff --git a/jobs/Backend/Task/ExchangeRate.cs b/jobs/Backend/Task/ExchangeRate.cs index 58c5bb10e..dc05cab45 100644 --- a/jobs/Backend/Task/ExchangeRate.cs +++ b/jobs/Backend/Task/ExchangeRate.cs @@ -1,23 +1,22 @@ -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater; + +public class ExchangeRate { - public class ExchangeRate + public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value) { - public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value) - { - SourceCurrency = sourceCurrency; - TargetCurrency = targetCurrency; - Value = value; - } + SourceCurrency = sourceCurrency; + TargetCurrency = targetCurrency; + Value = value; + } - public Currency SourceCurrency { get; } + public Currency SourceCurrency { get; } - public Currency TargetCurrency { get; } + public Currency TargetCurrency { get; } - public decimal Value { get; } + public decimal Value { get; } - public override string ToString() - { - return $"{SourceCurrency}/{TargetCurrency}={Value}"; - } + public override string ToString() + { + return $"{SourceCurrency}/{TargetCurrency}={Value}"; } } diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs index 6f82a97fb..9b88c5c89 100644 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/ExchangeRateProvider.cs @@ -1,19 +1,18 @@ using System.Collections.Generic; using System.Linq; -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater; + +public class ExchangeRateProvider { - public class ExchangeRateProvider + /// + /// Should return exchange rates among the specified currencies that are defined by the source. But only those defined + /// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK", + /// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide + /// some of the currencies, ignore them. + /// + public IEnumerable GetExchangeRates(IEnumerable currencies) { - /// - /// Should return exchange rates among the specified currencies that are defined by the source. But only those defined - /// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK", - /// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide - /// some of the currencies, ignore them. - /// - public IEnumerable GetExchangeRates(IEnumerable currencies) - { - return Enumerable.Empty(); - } + return Enumerable.Empty(); } } diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 379a69b1f..307544898 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -2,42 +2,41 @@ using System.Collections.Generic; using System.Linq; -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater; + +public static class Program { - public static class Program + private static IEnumerable currencies = new[] { - private static IEnumerable currencies = new[] - { - new Currency("USD"), - new Currency("EUR"), - new Currency("CZK"), - new Currency("JPY"), - new Currency("KES"), - new Currency("RUB"), - new Currency("THB"), - new Currency("TRY"), - new Currency("XYZ") - }; + new Currency("USD"), + new Currency("EUR"), + new Currency("CZK"), + new Currency("JPY"), + new Currency("KES"), + new Currency("RUB"), + new Currency("THB"), + new Currency("TRY"), + new Currency("XYZ") + }; - public static void Main(string[] args) + public static void Main(string[] args) + { + try { - try - { - var provider = new ExchangeRateProvider(); - var rates = provider.GetExchangeRates(currencies); + var provider = new ExchangeRateProvider(); + var rates = provider.GetExchangeRates(currencies); - Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); - foreach (var rate in rates) - { - Console.WriteLine(rate.ToString()); - } - } - catch (Exception e) + Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); + foreach (var rate in rates) { - Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); + Console.WriteLine(rate.ToString()); } - - Console.ReadLine(); } + catch (Exception e) + { + Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); + } + + Console.ReadLine(); } } diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs index c3479a47c..cd7a3c522 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs @@ -1,16 +1,15 @@ -namespace TestExchangeRateUpdater.Tests +namespace TestExchangeRateUpdater.Tests; + +public class Tests { - public class Tests + [SetUp] + public void Setup() { - [SetUp] - public void Setup() - { - } + } - [Test] - public void Test1() - { - Assert.Pass(); - } + [Test] + public void Test1() + { + Assert.Pass(); } } \ No newline at end of file From f8b3c83e96a90a9e508d3b62659648389d4e3b64 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 13:56:29 +0000 Subject: [PATCH 06/35] Minor: Added failing test for ExchangeRateService --- .../Services/ExchangeRateServiceTests.cs | 21 +++++++++++++++++++ .../UnitTest1.cs | 15 ------------- 2 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs delete mode 100644 jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs new file mode 100644 index 000000000..6ddbb053f --- /dev/null +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TestExchangeRateUpdater.Tests.Services; + +[TestFixture] +public class ExchangeRateServiceTests +{ + [Test] + public void ExchangeRateService_ShouldBe_InstanceOfExchangeRateService() + { + // Arrange + var exchangeRateService = new ExchangeRateService(); + + // Assert + exchangeRateService.Should().BeOfType(); + } +} diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs deleted file mode 100644 index cd7a3c522..000000000 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/UnitTest1.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace TestExchangeRateUpdater.Tests; - -public class Tests -{ - [SetUp] - public void Setup() - { - } - - [Test] - public void Test1() - { - Assert.Pass(); - } -} \ No newline at end of file From f0d3871608512aa7ce3c051d923e915e0475eece Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 14:04:21 +0000 Subject: [PATCH 07/35] Minor: Removed unused using across ExchangeRateUpdater, added ExchangeRateService test passes --- jobs/Backend/Task/Services/ExchangeRateService.cs | 5 +++++ .../Services/ExchangeRateServiceTests.cs | 8 ++------ .../TestExchangeRateUpdater.Tests.csproj | 4 ++++ 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 jobs/Backend/Task/Services/ExchangeRateService.cs diff --git a/jobs/Backend/Task/Services/ExchangeRateService.cs b/jobs/Backend/Task/Services/ExchangeRateService.cs new file mode 100644 index 000000000..9a560fe8e --- /dev/null +++ b/jobs/Backend/Task/Services/ExchangeRateService.cs @@ -0,0 +1,5 @@ +namespace ExchangeRateUpdater.Services; + +public class ExchangeRateService +{ +} diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs index 6ddbb053f..608988fa9 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using ExchangeRateUpdater.Services; namespace TestExchangeRateUpdater.Tests.Services; @@ -16,6 +12,6 @@ public void ExchangeRateService_ShouldBe_InstanceOfExchangeRateService() var exchangeRateService = new ExchangeRateService(); // Assert - exchangeRateService.Should().BeOfType(); + Assert.That(exchangeRateService, Is.InstanceOf()); } } diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj b/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj index 57e362a4f..9493b4659 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj @@ -23,4 +23,8 @@ + + + + From 478da10006f6bcc9f2fa10fd2f450ab1a849959f Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 14:07:34 +0000 Subject: [PATCH 08/35] Minor: Adjusted ExchangeRateServiceTest to test call return type --- .../Services/ExchangeRateServiceTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs index 608988fa9..06c93f12d 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -6,12 +6,15 @@ namespace TestExchangeRateUpdater.Tests.Services; public class ExchangeRateServiceTests { [Test] - public void ExchangeRateService_ShouldBe_InstanceOfExchangeRateService() + public void CallingGetExchangeRates_ShouldReturn_ExchangeRatesDTO() { // Arrange var exchangeRateService = new ExchangeRateService(); + // Act + var actual = exchangeRateService.GetExchangeRates(); + // Assert - Assert.That(exchangeRateService, Is.InstanceOf()); + Assert.That(actual, Is.InstanceOf()); } } From ed5539380b85706c3e814e9256c88d142e289d5b Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 14:31:24 +0000 Subject: [PATCH 09/35] Minor: Added code to make CallingExchangeRates pass --- jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs | 5 +++++ jobs/Backend/Task/Services/ExchangeRateService.cs | 8 +++++++- .../Services/ExchangeRateServiceTests.cs | 5 +++-- 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs diff --git a/jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs b/jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs new file mode 100644 index 000000000..37f839a4e --- /dev/null +++ b/jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs @@ -0,0 +1,5 @@ +namespace ExchangeRateUpdater.DTOs; + +public record ExchangeRatesDTO +{ +} diff --git a/jobs/Backend/Task/Services/ExchangeRateService.cs b/jobs/Backend/Task/Services/ExchangeRateService.cs index 9a560fe8e..5ff80b03e 100644 --- a/jobs/Backend/Task/Services/ExchangeRateService.cs +++ b/jobs/Backend/Task/Services/ExchangeRateService.cs @@ -1,5 +1,11 @@ -namespace ExchangeRateUpdater.Services; +using ExchangeRateUpdater.DTOs; + +namespace ExchangeRateUpdater.Services; public class ExchangeRateService { + public ExchangeRatesDTO GetExchangeRates() + { + return new ExchangeRatesDTO(); + } } diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs index 06c93f12d..5e812a5a1 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -1,4 +1,5 @@ -using ExchangeRateUpdater.Services; +using ExchangeRateUpdater.DTOs; +using ExchangeRateUpdater.Services; namespace TestExchangeRateUpdater.Tests.Services; @@ -15,6 +16,6 @@ public void CallingGetExchangeRates_ShouldReturn_ExchangeRatesDTO() var actual = exchangeRateService.GetExchangeRates(); // Assert - Assert.That(actual, Is.InstanceOf()); + Assert.That(actual, Is.InstanceOf()); } } From d0427953b1891913372330ca54c909a5a29216ef Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 14:33:26 +0000 Subject: [PATCH 10/35] Minor: Added global usings to ExchangeRateUpdater, added ExchangeRateDTO, used in ExchangeRatesDTO --- jobs/Backend/Task/DTOs/ExchangeRateDTO.cs | 27 ++++++++++++++++++++++ jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs | 6 ++++- jobs/Backend/Task/GlobalUsings.cs | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 jobs/Backend/Task/DTOs/ExchangeRateDTO.cs create mode 100644 jobs/Backend/Task/GlobalUsings.cs diff --git a/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs b/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs new file mode 100644 index 000000000..abb006fb4 --- /dev/null +++ b/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace ExchangeRateUpdater.DTOs; + +public record ExchangeRateDTO +{ + [JsonPropertyName("validFor")] + public string ValidFor { get; init; } + + [JsonPropertyName("order")] + public int Order { get; init; } + + [JsonPropertyName("currency")] + public string Currency { get; init; } + + [JsonPropertyName("country")] + public string Country { get; init; } + + [JsonPropertyName("amount")] + public int Amount { get; init; } + + [JsonPropertyName("currencyCode")] + public string CurrencyCode { get; init; } + + [JsonPropertyName("rate")] + public decimal Rate { get; init; } +} diff --git a/jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs b/jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs index 37f839a4e..f4e2afd37 100644 --- a/jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs +++ b/jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs @@ -1,5 +1,9 @@ -namespace ExchangeRateUpdater.DTOs; +using System.Text.Json.Serialization; + +namespace ExchangeRateUpdater.DTOs; public record ExchangeRatesDTO { + [JsonPropertyName("rates")] + public IEnumerable Rates { get; init; } } diff --git a/jobs/Backend/Task/GlobalUsings.cs b/jobs/Backend/Task/GlobalUsings.cs new file mode 100644 index 000000000..d978c77ae --- /dev/null +++ b/jobs/Backend/Task/GlobalUsings.cs @@ -0,0 +1 @@ +global using System.Collections.Generic; From 2d58aa50d2136a493378ff98ca729ee3a93267a7 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 14:34:19 +0000 Subject: [PATCH 11/35] Minor: Added setup to ExchangeRateServiceTests, added expected return --- .../Services/ExchangeRateServiceTests.cs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs index 5e812a5a1..5c930659c 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -6,16 +6,39 @@ namespace TestExchangeRateUpdater.Tests.Services; [TestFixture] public class ExchangeRateServiceTests { + private ExchangeRateService _exchangeRateService; + + [SetUp] + public void Setup() + { + _exchangeRateService = new ExchangeRateService(); + } + [Test] - public void CallingGetExchangeRates_ShouldReturn_ExchangeRatesDTO() + public void GetExchangeRates_ShouldReturn_CorrectExchangeRatesDTO() { // Arrange - var exchangeRateService = new ExchangeRateService(); + var expected = new ExchangeRatesDTO + { + Rates = new List + { + new () + { + ValidFor = "2025-01-11", + Order = 1, + Currency = "Currency", + Country = "Country", + Amount = 1, + CurrencyCode = "CUR", + Rate = 10.00M + } + } + }; // Act - var actual = exchangeRateService.GetExchangeRates(); + var actual = _exchangeRateService.GetExchangeRates(); // Assert - Assert.That(actual, Is.InstanceOf()); + Assert.That(actual.Rates, Is.EqualTo(expected.Rates)); } } From e71f82232b5e6b01a071c7490fd9911095e79618 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sat, 11 Jan 2025 14:34:55 +0000 Subject: [PATCH 12/35] Minor: Hardcoded ExchangeRateServive to pass test --- jobs/Backend/Task/Program.cs | 1 - .../Task/Services/ExchangeRateService.cs | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 307544898..9aae5a369 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; namespace ExchangeRateUpdater; diff --git a/jobs/Backend/Task/Services/ExchangeRateService.cs b/jobs/Backend/Task/Services/ExchangeRateService.cs index 5ff80b03e..9f1fe7bd7 100644 --- a/jobs/Backend/Task/Services/ExchangeRateService.cs +++ b/jobs/Backend/Task/Services/ExchangeRateService.cs @@ -1,4 +1,5 @@ using ExchangeRateUpdater.DTOs; +using System.Net.Http; namespace ExchangeRateUpdater.Services; @@ -6,6 +7,21 @@ public class ExchangeRateService { public ExchangeRatesDTO GetExchangeRates() { - return new ExchangeRatesDTO(); + return new ExchangeRatesDTO + { + Rates = new List + { + new () + { + ValidFor = "2025-01-11", + Order = 1, + Currency = "Currency", + Country = "Country", + Amount = 1, + CurrencyCode = "CUR", + Rate = 10.00M + } + } + }; } } From eadf184bea4a9f2f041cfcb77ad18b53c23ee560 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sun, 12 Jan 2025 10:02:05 +0000 Subject: [PATCH 13/35] Minor: Renamed test project, added mocked http call to ExchangeRateServiceTests --- jobs/Backend/Task/ExchangeRateUpdater.sln | 2 +- ...sproj => ExchangeRateUpdater.Tests.csproj} | 2 ++ .../Services/ExchangeRateServiceTests.cs | 29 +++++++++++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) rename jobs/Backend/TestExchangeRateUpdater.Tests/{TestExchangeRateUpdater.Tests.csproj => ExchangeRateUpdater.Tests.csproj} (86%) diff --git a/jobs/Backend/Task/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater.sln index 9f4d88626..c2588c95a 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.sln +++ b/jobs/Backend/Task/ExchangeRateUpdater.sln @@ -5,7 +5,7 @@ VisualStudioVersion = 17.8.34309.116 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestExchangeRateUpdater.Tests", "..\TestExchangeRateUpdater.Tests\TestExchangeRateUpdater.Tests.csproj", "{D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.Tests", "..\TestExchangeRateUpdater.Tests\ExchangeRateUpdater.Tests.csproj", "{D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj b/jobs/Backend/TestExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj similarity index 86% rename from jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj rename to jobs/Backend/TestExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj index 9493b4659..8cdde4199 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/TestExchangeRateUpdater.Tests.csproj +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj @@ -10,7 +10,9 @@ + + diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs index 5c930659c..4be0938ed 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -1,5 +1,8 @@ using ExchangeRateUpdater.DTOs; using ExchangeRateUpdater.Services; +using Moq; +using Moq.Protected; +using System.Net; namespace TestExchangeRateUpdater.Tests.Services; @@ -11,7 +14,29 @@ public class ExchangeRateServiceTests [SetUp] public void Setup() { - _exchangeRateService = new ExchangeRateService(); + var handlerMock = new Mock(); + handlerMock + .Protected() + .Setup>( + "SendAsync", + It.IsAny(), + It.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent( + "{\"rates\": [{\"validFor\": \"2025-01-11\", \"order\": 1, \"currency\": \"Currency\", \"country\": \"Country\", \"amount\": 1, \"currencyCode\": \"CUR\", \"rate\": 10.00}]}" + ) + }) + .Verifiable(); + var httpClient = new HttpClient(handlerMock.Object); + var httpClientFactory = new Mock(); + httpClientFactory + .Setup(_ => _.CreateClient(It.IsAny())) + .Returns(httpClient); + + _exchangeRateService = new ExchangeRateService(httpClientFactory.Object); + } [Test] @@ -36,7 +61,7 @@ public void GetExchangeRates_ShouldReturn_CorrectExchangeRatesDTO() }; // Act - var actual = _exchangeRateService.GetExchangeRates(); + var actual = _exchangeRateService.GetExchangeRates().Result; // Assert Assert.That(actual.Rates, Is.EqualTo(expected.Rates)); From acbe8ca7d5e177e0379087607f1449bea4806f88 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sun, 12 Jan 2025 10:19:11 +0000 Subject: [PATCH 14/35] Minor: Extracted logic from Program.cs to ExchangeRateUpdater (temporary), added Hosting and HTTP to allow for IHttpClientFactory implementation --- jobs/Backend/Task/ExchangeRateUpdater.cs | 42 +++++++++++++++++ jobs/Backend/Task/ExchangeRateUpdater.csproj | 5 +++ jobs/Backend/Task/Program.cs | 47 ++++---------------- 3 files changed, 56 insertions(+), 38 deletions(-) create mode 100644 jobs/Backend/Task/ExchangeRateUpdater.cs diff --git a/jobs/Backend/Task/ExchangeRateUpdater.cs b/jobs/Backend/Task/ExchangeRateUpdater.cs new file mode 100644 index 000000000..a47a9430d --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater.cs @@ -0,0 +1,42 @@ +using System; +using System.Linq; + +namespace ExchangeRateUpdater +{ + public class ExchangeRateUpdater + { + private readonly IEnumerable currencies = new[] + { + new Currency("USD"), + new Currency("EUR"), + new Currency("CZK"), + new Currency("JPY"), + new Currency("KES"), + new Currency("RUB"), + new Currency("THB"), + new Currency("TRY"), + new Currency("XYZ") + }; + + public void DisplayExchangeRates() + { + try + { + var provider = new ExchangeRateProvider(); + var rates = provider.GetExchangeRates(currencies); + + Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); + foreach (var rate in rates) + { + Console.WriteLine(rate.ToString()); + } + } + catch (Exception e) + { + Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); + } + + Console.ReadLine(); + } + } +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 2215fc8e9..18437f4f4 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -5,4 +5,9 @@ net8.0 + + + + + \ No newline at end of file diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 9aae5a369..fbba59dd6 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,41 +1,12 @@ -using System; -using System.Linq; +using ExchangeRateUpdater.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; -namespace ExchangeRateUpdater; -public static class Program -{ - private static IEnumerable currencies = new[] +var host = new HostBuilder() + .ConfigureServices(services => { - new Currency("USD"), - new Currency("EUR"), - new Currency("CZK"), - new Currency("JPY"), - new Currency("KES"), - new Currency("RUB"), - new Currency("THB"), - new Currency("TRY"), - new Currency("XYZ") - }; - - public static void Main(string[] args) - { - try - { - var provider = new ExchangeRateProvider(); - var rates = provider.GetExchangeRates(currencies); - - Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); - foreach (var rate in rates) - { - Console.WriteLine(rate.ToString()); - } - } - catch (Exception e) - { - Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); - } - - Console.ReadLine(); - } -} + services.AddHttpClient(); + services.AddTransient(); + }) + .Build(); From d5f3b7ef71ebf906b4dd0d9c7a543006a7559004 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sun, 12 Jan 2025 10:20:56 +0000 Subject: [PATCH 15/35] Minor: File scoped temp updater --- jobs/Backend/Task/ExchangeRateUpdater.cs | 57 ++++++++++++------------ 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/jobs/Backend/Task/ExchangeRateUpdater.cs b/jobs/Backend/Task/ExchangeRateUpdater.cs index a47a9430d..095e5beda 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.cs +++ b/jobs/Backend/Task/ExchangeRateUpdater.cs @@ -1,42 +1,41 @@ using System; using System.Linq; -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater; + +public class ExchangeRateUpdater { - public class ExchangeRateUpdater + private readonly IEnumerable currencies = new[] { - private readonly IEnumerable currencies = new[] - { - new Currency("USD"), - new Currency("EUR"), - new Currency("CZK"), - new Currency("JPY"), - new Currency("KES"), - new Currency("RUB"), - new Currency("THB"), - new Currency("TRY"), - new Currency("XYZ") - }; + new Currency("USD"), + new Currency("EUR"), + new Currency("CZK"), + new Currency("JPY"), + new Currency("KES"), + new Currency("RUB"), + new Currency("THB"), + new Currency("TRY"), + new Currency("XYZ") +}; - public void DisplayExchangeRates() + public void DisplayExchangeRates() + { + try { - try - { - var provider = new ExchangeRateProvider(); - var rates = provider.GetExchangeRates(currencies); + var provider = new ExchangeRateProvider(); + var rates = provider.GetExchangeRates(currencies); - Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); - foreach (var rate in rates) - { - Console.WriteLine(rate.ToString()); - } - } - catch (Exception e) + Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); + foreach (var rate in rates) { - Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); + Console.WriteLine(rate.ToString()); } - - Console.ReadLine(); } + catch (Exception e) + { + Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); + } + + Console.ReadLine(); } } From 2d3aafad573ebd4cfcfcabb9338e743fcf8cb6ea Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sun, 12 Jan 2025 10:26:44 +0000 Subject: [PATCH 16/35] Minor: Added basic HttpClient via factory to ExchangeRateService --- .../Task/Services/ExchangeRateService.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/jobs/Backend/Task/Services/ExchangeRateService.cs b/jobs/Backend/Task/Services/ExchangeRateService.cs index 9f1fe7bd7..f67b07b4e 100644 --- a/jobs/Backend/Task/Services/ExchangeRateService.cs +++ b/jobs/Backend/Task/Services/ExchangeRateService.cs @@ -1,27 +1,27 @@ using ExchangeRateUpdater.DTOs; using System.Net.Http; +using System.Threading.Tasks; +using System.Text.Json; namespace ExchangeRateUpdater.Services; public class ExchangeRateService { - public ExchangeRatesDTO GetExchangeRates() + private IHttpClientFactory _httpClientFactory; + + public ExchangeRateService() { - return new ExchangeRatesDTO - { - Rates = new List - { - new () - { - ValidFor = "2025-01-11", - Order = 1, - Currency = "Currency", - Country = "Country", - Amount = 1, - CurrencyCode = "CUR", - Rate = 10.00M - } - } - }; + } + + public ExchangeRateService(IHttpClientFactory httpClientFactory) + => _httpClientFactory = httpClientFactory; + + public async Task GetExchangeRates() + { + var httpClient = _httpClientFactory.CreateClient(); + + var result = await httpClient.GetAsync("https://api.cnb.cz/cnbapi/exrates/daily?date=2019-05-17&lang=EN"); + + return JsonSerializer.Deserialize(result.Content.ReadAsStringAsync().Result); } } From 9709ff58e78908327d3e4de36bd84e1e65efcfde Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sun, 12 Jan 2025 10:35:53 +0000 Subject: [PATCH 17/35] Minor: Removed paramaterless constructor from ExchangeRateService, added interface, changed transient service registration to use interface --- jobs/Backend/Task/Program.cs | 2 +- jobs/Backend/Task/Services/ExchangeRateService.cs | 6 +----- jobs/Backend/Task/Services/IExchangeRateService.cs | 9 +++++++++ 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 jobs/Backend/Task/Services/IExchangeRateService.cs diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index fbba59dd6..260542daf 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -7,6 +7,6 @@ .ConfigureServices(services => { services.AddHttpClient(); - services.AddTransient(); + services.AddTransient(); }) .Build(); diff --git a/jobs/Backend/Task/Services/ExchangeRateService.cs b/jobs/Backend/Task/Services/ExchangeRateService.cs index f67b07b4e..22c4f7b4e 100644 --- a/jobs/Backend/Task/Services/ExchangeRateService.cs +++ b/jobs/Backend/Task/Services/ExchangeRateService.cs @@ -5,14 +5,10 @@ namespace ExchangeRateUpdater.Services; -public class ExchangeRateService +public class ExchangeRateService: IExchangeRateService { private IHttpClientFactory _httpClientFactory; - public ExchangeRateService() - { - } - public ExchangeRateService(IHttpClientFactory httpClientFactory) => _httpClientFactory = httpClientFactory; diff --git a/jobs/Backend/Task/Services/IExchangeRateService.cs b/jobs/Backend/Task/Services/IExchangeRateService.cs new file mode 100644 index 000000000..a53ee600b --- /dev/null +++ b/jobs/Backend/Task/Services/IExchangeRateService.cs @@ -0,0 +1,9 @@ +using ExchangeRateUpdater.DTOs; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Services; + +public interface IExchangeRateService +{ + Task GetExchangeRates(); +} From c50839b07936f261c02a498c77e863cb9216bd18 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sun, 12 Jan 2025 15:11:43 +0000 Subject: [PATCH 18/35] Minor: Reduced ExchangeRateDTO to required properties only, added test for non success response --- jobs/Backend/Task/DTOs/ExchangeRateDTO.cs | 15 ------ .../Services/ExchangeRateServiceTests.cs | 49 ++++++++++++++----- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs b/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs index abb006fb4..009984c96 100644 --- a/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs +++ b/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs @@ -4,21 +4,6 @@ namespace ExchangeRateUpdater.DTOs; public record ExchangeRateDTO { - [JsonPropertyName("validFor")] - public string ValidFor { get; init; } - - [JsonPropertyName("order")] - public int Order { get; init; } - - [JsonPropertyName("currency")] - public string Currency { get; init; } - - [JsonPropertyName("country")] - public string Country { get; init; } - - [JsonPropertyName("amount")] - public int Amount { get; init; } - [JsonPropertyName("currencyCode")] public string CurrencyCode { get; init; } diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs index 4be0938ed..5b6d1236d 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -3,6 +3,7 @@ using Moq; using Moq.Protected; using System.Net; +using System.Net.Http; namespace TestExchangeRateUpdater.Tests.Services; @@ -19,16 +20,16 @@ public void Setup() .Protected() .Setup>( "SendAsync", - It.IsAny(), - It.IsAny()) + ItExpr.IsAny(), + ItExpr.IsAny()) .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, - Content = new StringContent( + Content = new StringContent + ( "{\"rates\": [{\"validFor\": \"2025-01-11\", \"order\": 1, \"currency\": \"Currency\", \"country\": \"Country\", \"amount\": 1, \"currencyCode\": \"CUR\", \"rate\": 10.00}]}" - ) - }) - .Verifiable(); + ) + }); var httpClient = new HttpClient(handlerMock.Object); var httpClientFactory = new Mock(); httpClientFactory @@ -49,11 +50,6 @@ public void GetExchangeRates_ShouldReturn_CorrectExchangeRatesDTO() { new () { - ValidFor = "2025-01-11", - Order = 1, - Currency = "Currency", - Country = "Country", - Amount = 1, CurrencyCode = "CUR", Rate = 10.00M } @@ -66,4 +62,35 @@ public void GetExchangeRates_ShouldReturn_CorrectExchangeRatesDTO() // Assert Assert.That(actual.Rates, Is.EqualTo(expected.Rates)); } + + [Test] + public void GetExchangeRates_ShouldThrowException_WhenResponseIsNotSuccessful() + { + // Arrange + var handlerMock = new Mock(); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.BadRequest + }); + var httpClient = new HttpClient(handlerMock.Object); + var httpClientFactory = new Mock(); + httpClientFactory + .Setup(_ => _.CreateClient(It.IsAny())) + .Returns(httpClient); + + _exchangeRateService = new ExchangeRateService(httpClientFactory.Object); + + // Assert + Assert.Multiple(() => + { + var exception = Assert.ThrowsAsync(() => _exchangeRateService.GetExchangeRates()); + Assert.That(exception.Message, Is.EqualTo("Failed to get exchange rates. Status code: BadRequest")); + }); + } } From 99b2e5ee544c65d940341d2124e6042318b5f6a9 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sun, 12 Jan 2025 15:12:57 +0000 Subject: [PATCH 19/35] Handle non response status code in ExchangeRateService --- jobs/Backend/Task/Services/ExchangeRateService.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/jobs/Backend/Task/Services/ExchangeRateService.cs b/jobs/Backend/Task/Services/ExchangeRateService.cs index 22c4f7b4e..e30733b50 100644 --- a/jobs/Backend/Task/Services/ExchangeRateService.cs +++ b/jobs/Backend/Task/Services/ExchangeRateService.cs @@ -2,6 +2,8 @@ using System.Net.Http; using System.Threading.Tasks; using System.Text.Json; +using System; +using System.Net.Http.Json; namespace ExchangeRateUpdater.Services; @@ -9,15 +11,22 @@ public class ExchangeRateService: IExchangeRateService { private IHttpClientFactory _httpClientFactory; - public ExchangeRateService(IHttpClientFactory httpClientFactory) + public ExchangeRateService(IHttpClientFactory httpClientFactory) => _httpClientFactory = httpClientFactory; public async Task GetExchangeRates() { var httpClient = _httpClientFactory.CreateClient(); - var result = await httpClient.GetAsync("https://api.cnb.cz/cnbapi/exrates/daily?date=2019-05-17&lang=EN"); + var response = await httpClient.GetAsync("https://api.cnb.cz/cnbapi/exrates/daily?date=2025-11-01&lang=EN"); - return JsonSerializer.Deserialize(result.Content.ReadAsStringAsync().Result); + if (!response.IsSuccessStatusCode) + { + throw new Exception($"Failed to get exchange rates. Status code: {response.StatusCode}"); + } + + var result = await response.Content.ReadFromJsonAsync(); + + return result; } } From 02d09d30d215632d58ebd550f1962abc948db1f4 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sun, 12 Jan 2025 16:48:54 +0000 Subject: [PATCH 20/35] Minor: Added console logging, switched to typed HttpClientFactory pattern, changed tests to suit new pattern --- jobs/Backend/Task/ExchangeRateUpdater.csproj | 1 + jobs/Backend/Task/Program.cs | 14 ++++++++++++-- jobs/Backend/Task/Services/ExchangeRateService.cs | 13 +++++++------ .../Services/ExchangeRateServiceTests.cs | 15 ++++----------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 18437f4f4..6b31352da 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -8,6 +8,7 @@ + \ No newline at end of file diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 260542daf..eefef47d5 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,12 +1,22 @@ using ExchangeRateUpdater.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; var host = new HostBuilder() .ConfigureServices(services => { - services.AddHttpClient(); - services.AddTransient(); + services.AddHttpClient( + client => + { + client.BaseAddress = new Uri("https://api.cnb.cz/cnbapi/"); + }); + services.AddLogging(); + }) + .ConfigureLogging(logging => + { + logging.AddConsole(); }) .Build(); diff --git a/jobs/Backend/Task/Services/ExchangeRateService.cs b/jobs/Backend/Task/Services/ExchangeRateService.cs index e30733b50..fbdc6a326 100644 --- a/jobs/Backend/Task/Services/ExchangeRateService.cs +++ b/jobs/Backend/Task/Services/ExchangeRateService.cs @@ -4,21 +4,22 @@ using System.Text.Json; using System; using System.Net.Http.Json; +using Microsoft.Extensions.Logging; namespace ExchangeRateUpdater.Services; public class ExchangeRateService: IExchangeRateService { - private IHttpClientFactory _httpClientFactory; + private HttpClient _httpClient; - public ExchangeRateService(IHttpClientFactory httpClientFactory) - => _httpClientFactory = httpClientFactory; + public ExchangeRateService(HttpClient httpClient) + { + _httpClient = httpClient; + } public async Task GetExchangeRates() { - var httpClient = _httpClientFactory.CreateClient(); - - var response = await httpClient.GetAsync("https://api.cnb.cz/cnbapi/exrates/daily?date=2025-11-01&lang=EN"); + var response = await _httpClient.GetAsync("exrates/daily?date=2025-11-01&lang=EN"); if (!response.IsSuccessStatusCode) { diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs index 5b6d1236d..899b4c860 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -31,12 +31,8 @@ public void Setup() ) }); var httpClient = new HttpClient(handlerMock.Object); - var httpClientFactory = new Mock(); - httpClientFactory - .Setup(_ => _.CreateClient(It.IsAny())) - .Returns(httpClient); - - _exchangeRateService = new ExchangeRateService(httpClientFactory.Object); + httpClient.BaseAddress = new Uri("https://baseaddress/"); + _exchangeRateService = new ExchangeRateService(httpClient); } @@ -79,12 +75,9 @@ public void GetExchangeRates_ShouldThrowException_WhenResponseIsNotSuccessful() StatusCode = HttpStatusCode.BadRequest }); var httpClient = new HttpClient(handlerMock.Object); - var httpClientFactory = new Mock(); - httpClientFactory - .Setup(_ => _.CreateClient(It.IsAny())) - .Returns(httpClient); + httpClient.BaseAddress = new Uri("https://baseaddress/"); - _exchangeRateService = new ExchangeRateService(httpClientFactory.Object); + _exchangeRateService = new ExchangeRateService(httpClient); // Assert Assert.Multiple(() => From 37e3f340a7d264a462eda012e803d1ae07766ddd Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sun, 12 Jan 2025 16:50:22 +0000 Subject: [PATCH 21/35] Minor: Removed usings, moved currency and exchangerate to models --- jobs/Backend/Task/ExchangeRateProvider.cs | 4 ++-- jobs/Backend/Task/ExchangeRateUpdater.cs | 1 + jobs/Backend/Task/{ => Models}/Currency.cs | 2 +- jobs/Backend/Task/{ => Models}/ExchangeRate.cs | 2 +- jobs/Backend/Task/Services/ExchangeRateService.cs | 2 -- .../Services/ExchangeRateServiceTests.cs | 1 - 6 files changed, 5 insertions(+), 7 deletions(-) rename jobs/Backend/Task/{ => Models}/Currency.cs (87%) rename jobs/Backend/Task/{ => Models}/ExchangeRate.cs (92%) diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs index 9b88c5c89..3d0f9da0b 100644 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/ExchangeRateProvider.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using System.Linq; +using System.Linq; +using ExchangeRateUpdater.Models; namespace ExchangeRateUpdater; diff --git a/jobs/Backend/Task/ExchangeRateUpdater.cs b/jobs/Backend/Task/ExchangeRateUpdater.cs index 095e5beda..da6d4c587 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.cs +++ b/jobs/Backend/Task/ExchangeRateUpdater.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using ExchangeRateUpdater.Models; namespace ExchangeRateUpdater; diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/Models/Currency.cs similarity index 87% rename from jobs/Backend/Task/Currency.cs rename to jobs/Backend/Task/Models/Currency.cs index 057eb8183..0f8d897b1 100644 --- a/jobs/Backend/Task/Currency.cs +++ b/jobs/Backend/Task/Models/Currency.cs @@ -1,4 +1,4 @@ -namespace ExchangeRateUpdater; +namespace ExchangeRateUpdater.Models; public class Currency { diff --git a/jobs/Backend/Task/ExchangeRate.cs b/jobs/Backend/Task/Models/ExchangeRate.cs similarity index 92% rename from jobs/Backend/Task/ExchangeRate.cs rename to jobs/Backend/Task/Models/ExchangeRate.cs index dc05cab45..a415ffa1d 100644 --- a/jobs/Backend/Task/ExchangeRate.cs +++ b/jobs/Backend/Task/Models/ExchangeRate.cs @@ -1,4 +1,4 @@ -namespace ExchangeRateUpdater; +namespace ExchangeRateUpdater.Models; public class ExchangeRate { diff --git a/jobs/Backend/Task/Services/ExchangeRateService.cs b/jobs/Backend/Task/Services/ExchangeRateService.cs index fbdc6a326..345afd37d 100644 --- a/jobs/Backend/Task/Services/ExchangeRateService.cs +++ b/jobs/Backend/Task/Services/ExchangeRateService.cs @@ -1,10 +1,8 @@ using ExchangeRateUpdater.DTOs; using System.Net.Http; using System.Threading.Tasks; -using System.Text.Json; using System; using System.Net.Http.Json; -using Microsoft.Extensions.Logging; namespace ExchangeRateUpdater.Services; diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs index 899b4c860..de2269534 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -3,7 +3,6 @@ using Moq; using Moq.Protected; using System.Net; -using System.Net.Http; namespace TestExchangeRateUpdater.Tests.Services; From 33996bc1f740a564acc50ec8bd45929f1a813bca Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sun, 12 Jan 2025 18:25:54 +0000 Subject: [PATCH 22/35] Minor: Added TimeProvider to abstract time, changed call in ExchangeRateService to use todays date, Removed temp ExchangeRateUpdater --- jobs/Backend/Task/ExchangeRateProvider.cs | 36 +++++++++++----- jobs/Backend/Task/ExchangeRateUpdater.cs | 42 ------------------- jobs/Backend/Task/IExchangeRateProvider.cs | 15 +++++++ jobs/Backend/Task/Program.cs | 40 +++++++++++++++++- .../Task/Services/ExchangeRateService.cs | 9 ++-- .../Services/ExchangeRateServiceTests.cs | 7 +++- 6 files changed, 90 insertions(+), 59 deletions(-) delete mode 100644 jobs/Backend/Task/ExchangeRateUpdater.cs create mode 100644 jobs/Backend/Task/IExchangeRateProvider.cs diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs index 3d0f9da0b..c35a19dbf 100644 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/ExchangeRateProvider.cs @@ -1,18 +1,34 @@ using System.Linq; +using System.Threading.Tasks; using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Services; namespace ExchangeRateUpdater; -public class ExchangeRateProvider -{ - /// - /// Should return exchange rates among the specified currencies that are defined by the source. But only those defined - /// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK", - /// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide - /// some of the currencies, ignore them. - /// - public IEnumerable GetExchangeRates(IEnumerable currencies) +public class ExchangeRateProvider : IExchangeRateProvider +{ + private readonly IExchangeRateService _exchangeRateService; + + public ExchangeRateProvider(IExchangeRateService exchangeRateService) { - return Enumerable.Empty(); + _exchangeRateService = exchangeRateService; + } + + /// + public async Task> GetExchangeRates(IEnumerable currencies) + { + var exchangeRates = new List(); + var exchangeRatesFromService = await _exchangeRateService.GetExchangeRates(); + + foreach (var currency in currencies) + { + var exchangeRate = exchangeRatesFromService.Rates.FirstOrDefault(rate => rate.CurrencyCode == currency.Code); + if (exchangeRate != null) + { + exchangeRates.Add(new ExchangeRate(currency, new Currency("CZK"), exchangeRate.Rate)); + } + } + + return exchangeRates; } } diff --git a/jobs/Backend/Task/ExchangeRateUpdater.cs b/jobs/Backend/Task/ExchangeRateUpdater.cs deleted file mode 100644 index da6d4c587..000000000 --- a/jobs/Backend/Task/ExchangeRateUpdater.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Linq; -using ExchangeRateUpdater.Models; - -namespace ExchangeRateUpdater; - -public class ExchangeRateUpdater -{ - private readonly IEnumerable currencies = new[] - { - new Currency("USD"), - new Currency("EUR"), - new Currency("CZK"), - new Currency("JPY"), - new Currency("KES"), - new Currency("RUB"), - new Currency("THB"), - new Currency("TRY"), - new Currency("XYZ") -}; - - public void DisplayExchangeRates() - { - try - { - var provider = new ExchangeRateProvider(); - var rates = provider.GetExchangeRates(currencies); - - Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); - foreach (var rate in rates) - { - Console.WriteLine(rate.ToString()); - } - } - catch (Exception e) - { - Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); - } - - Console.ReadLine(); - } -} diff --git a/jobs/Backend/Task/IExchangeRateProvider.cs b/jobs/Backend/Task/IExchangeRateProvider.cs new file mode 100644 index 000000000..7b74f82f5 --- /dev/null +++ b/jobs/Backend/Task/IExchangeRateProvider.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using ExchangeRateUpdater.Models; + +namespace ExchangeRateUpdater; + +public interface IExchangeRateProvider +{ + /// + /// Should return exchange rates among the specified currencies that are defined by the source. But only those defined + /// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK", + /// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide + /// some of the currencies, ignore them. + /// + Task> GetExchangeRates(IEnumerable currencies); +} diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index eefef47d5..0e97c8ede 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,9 +1,11 @@ -using ExchangeRateUpdater.Services; +using ExchangeRateUpdater; +using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; - +using System.Linq; var host = new HostBuilder() .ConfigureServices(services => @@ -13,6 +15,8 @@ { client.BaseAddress = new Uri("https://api.cnb.cz/cnbapi/"); }); + services.AddTransient(); + services.AddSingleton(TimeProvider.System); services.AddLogging(); }) .ConfigureLogging(logging => @@ -20,3 +24,35 @@ logging.AddConsole(); }) .Build(); + +var exchangeRateProvider = host.Services.GetRequiredService(); + +IEnumerable currencies = new[] +{ + new Currency("USD"), + new Currency("EUR"), + new Currency("CZK"), + new Currency("JPY"), + new Currency("KES"), + new Currency("RUB"), + new Currency("THB"), + new Currency("TRY"), + new Currency("XYZ") +}; + +try +{ + var rates = exchangeRateProvider.GetExchangeRates(currencies).Result; + + Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); + foreach (var rate in rates) + { + Console.WriteLine(rate.ToString()); + } +} +catch (Exception e) +{ + Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); +} + +Console.ReadLine(); \ No newline at end of file diff --git a/jobs/Backend/Task/Services/ExchangeRateService.cs b/jobs/Backend/Task/Services/ExchangeRateService.cs index 345afd37d..7d73ea578 100644 --- a/jobs/Backend/Task/Services/ExchangeRateService.cs +++ b/jobs/Backend/Task/Services/ExchangeRateService.cs @@ -8,16 +8,19 @@ namespace ExchangeRateUpdater.Services; public class ExchangeRateService: IExchangeRateService { - private HttpClient _httpClient; + private readonly HttpClient _httpClient; + private readonly TimeProvider _timeProvider; - public ExchangeRateService(HttpClient httpClient) + public ExchangeRateService(HttpClient httpClient, TimeProvider timeProvider) { _httpClient = httpClient; + _timeProvider = timeProvider; } public async Task GetExchangeRates() { - var response = await _httpClient.GetAsync("exrates/daily?date=2025-11-01&lang=EN"); + var todaysDate = _timeProvider.GetUtcNow().ToString("yyyy-MM-dd"); + var response = await _httpClient.GetAsync($"exrates/daily?date={todaysDate}"); if (!response.IsSuccessStatusCode) { diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs index de2269534..02651e57c 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -10,6 +10,7 @@ namespace TestExchangeRateUpdater.Tests.Services; public class ExchangeRateServiceTests { private ExchangeRateService _exchangeRateService; + private Mock _timeProviderMock; [SetUp] public void Setup() @@ -31,7 +32,9 @@ public void Setup() }); var httpClient = new HttpClient(handlerMock.Object); httpClient.BaseAddress = new Uri("https://baseaddress/"); - _exchangeRateService = new ExchangeRateService(httpClient); + _timeProviderMock = new Mock(); + _timeProviderMock.Setup(x => x.GetUtcNow()).Returns(new DateTime(2025, 1, 11)); + _exchangeRateService = new ExchangeRateService(httpClient, _timeProviderMock.Object); } @@ -76,7 +79,7 @@ public void GetExchangeRates_ShouldThrowException_WhenResponseIsNotSuccessful() var httpClient = new HttpClient(handlerMock.Object); httpClient.BaseAddress = new Uri("https://baseaddress/"); - _exchangeRateService = new ExchangeRateService(httpClient); + _exchangeRateService = new ExchangeRateService(httpClient, _timeProviderMock.Object); // Assert Assert.Multiple(() => From 307c7befe5af495123f8e660ee99a16da716cc05 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Sun, 12 Jan 2025 22:12:29 +0000 Subject: [PATCH 23/35] Minor: Added amount back to Provider and adjusted tests --- jobs/Backend/Task/DTOs/ExchangeRateDTO.cs | 3 +++ jobs/Backend/Task/ExchangeRateProvider.cs | 3 ++- .../Services/ExchangeRateServiceTests.cs | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs b/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs index 009984c96..b699a2de2 100644 --- a/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs +++ b/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs @@ -4,6 +4,9 @@ namespace ExchangeRateUpdater.DTOs; public record ExchangeRateDTO { + [JsonPropertyName("amount")] + public int Amount { get; init; } + [JsonPropertyName("currencyCode")] public string CurrencyCode { get; init; } diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs index c35a19dbf..d1eb4e2ca 100644 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/ExchangeRateProvider.cs @@ -25,7 +25,8 @@ public async Task> GetExchangeRates(IEnumerable rate.CurrencyCode == currency.Code); if (exchangeRate != null) { - exchangeRates.Add(new ExchangeRate(currency, new Currency("CZK"), exchangeRate.Rate)); + var value = exchangeRate.Rate / exchangeRate.Amount; + exchangeRates.Add(new ExchangeRate(currency, new Currency("CZK"), value)); } } diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs index 02651e57c..34c72f89e 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -48,6 +48,7 @@ public void GetExchangeRates_ShouldReturn_CorrectExchangeRatesDTO() { new () { + Amount = 1, CurrencyCode = "CUR", Rate = 10.00M } From 148977aff006e1e37635a04c0bbbf6e61b8be450 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Tue, 14 Jan 2025 13:15:21 +0000 Subject: [PATCH 24/35] Minor: Renamed DTOs to singular --- jobs/Backend/Task/{DTOs => DTO}/ExchangeRateDTO.cs | 0 jobs/Backend/Task/{DTOs => DTO}/ExchangeRatesDTO.cs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename jobs/Backend/Task/{DTOs => DTO}/ExchangeRateDTO.cs (100%) rename jobs/Backend/Task/{DTOs => DTO}/ExchangeRatesDTO.cs (100%) diff --git a/jobs/Backend/Task/DTOs/ExchangeRateDTO.cs b/jobs/Backend/Task/DTO/ExchangeRateDTO.cs similarity index 100% rename from jobs/Backend/Task/DTOs/ExchangeRateDTO.cs rename to jobs/Backend/Task/DTO/ExchangeRateDTO.cs diff --git a/jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs b/jobs/Backend/Task/DTO/ExchangeRatesDTO.cs similarity index 100% rename from jobs/Backend/Task/DTOs/ExchangeRatesDTO.cs rename to jobs/Backend/Task/DTO/ExchangeRatesDTO.cs From 3dab86a703ef6ef85923396297a2ce14b54efcea Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Tue, 14 Jan 2025 13:16:24 +0000 Subject: [PATCH 25/35] Minor: Implemented and moved ExchangeRateProvider --- jobs/Backend/Task/ExchangeRateProvider.cs | 35 ------ .../Task/Providers/ExchangeRateProvider.cs | 42 +++++++ .../{ => Providers}/IExchangeRateProvider.cs | 4 +- .../Providers/ExchangeRateProviderTests.cs | 116 ++++++++++++++++++ 4 files changed, 160 insertions(+), 37 deletions(-) delete mode 100644 jobs/Backend/Task/ExchangeRateProvider.cs create mode 100644 jobs/Backend/Task/Providers/ExchangeRateProvider.cs rename jobs/Backend/Task/{ => Providers}/IExchangeRateProvider.cs (81%) create mode 100644 jobs/Backend/TestExchangeRateUpdater.Tests/Providers/ExchangeRateProviderTests.cs diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs deleted file mode 100644 index d1eb4e2ca..000000000 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using ExchangeRateUpdater.Models; -using ExchangeRateUpdater.Services; - -namespace ExchangeRateUpdater; - -public class ExchangeRateProvider : IExchangeRateProvider -{ - private readonly IExchangeRateService _exchangeRateService; - - public ExchangeRateProvider(IExchangeRateService exchangeRateService) - { - _exchangeRateService = exchangeRateService; - } - - /// - public async Task> GetExchangeRates(IEnumerable currencies) - { - var exchangeRates = new List(); - var exchangeRatesFromService = await _exchangeRateService.GetExchangeRates(); - - foreach (var currency in currencies) - { - var exchangeRate = exchangeRatesFromService.Rates.FirstOrDefault(rate => rate.CurrencyCode == currency.Code); - if (exchangeRate != null) - { - var value = exchangeRate.Rate / exchangeRate.Amount; - exchangeRates.Add(new ExchangeRate(currency, new Currency("CZK"), value)); - } - } - - return exchangeRates; - } -} diff --git a/jobs/Backend/Task/Providers/ExchangeRateProvider.cs b/jobs/Backend/Task/Providers/ExchangeRateProvider.cs new file mode 100644 index 000000000..bbd8f56b2 --- /dev/null +++ b/jobs/Backend/Task/Providers/ExchangeRateProvider.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Threading.Tasks; +using ExchangeRateUpdater.DTOs; +using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Services; +using Microsoft.Extensions.Logging; + +namespace ExchangeRateUpdater.Providers; + +public class ExchangeRateProvider : IExchangeRateProvider +{ + private readonly IExchangeRateService _exchangeRateService; + private readonly ILogger _logger; + + public ExchangeRateProvider(IExchangeRateService exchangeRateService, ILogger logger) + { + _exchangeRateService = exchangeRateService; + _logger = logger; + } + + /// + public IEnumerable GetExchangeRates(IEnumerable currencies) + { + var exchangeRates = _exchangeRateService.GetExchangeRatesAsync().Result; + + var filteredRates = exchangeRates.Rates + .Where(e => + currencies.Any(c => c.Code == e.CurrencyCode)) + .Select(rate => + new ExchangeRate(new Currency(rate.CurrencyCode), new Currency("CZK"), rate.Rate / rate.Amount)); + + if (filteredRates.Count() is 0) + { + _logger.LogInformation("No matching rates found for chosen currency codes"); + } + + return filteredRates; + } +} diff --git a/jobs/Backend/Task/IExchangeRateProvider.cs b/jobs/Backend/Task/Providers/IExchangeRateProvider.cs similarity index 81% rename from jobs/Backend/Task/IExchangeRateProvider.cs rename to jobs/Backend/Task/Providers/IExchangeRateProvider.cs index 7b74f82f5..fecfed3ab 100644 --- a/jobs/Backend/Task/IExchangeRateProvider.cs +++ b/jobs/Backend/Task/Providers/IExchangeRateProvider.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using ExchangeRateUpdater.Models; -namespace ExchangeRateUpdater; +namespace ExchangeRateUpdater.Providers; public interface IExchangeRateProvider { @@ -11,5 +11,5 @@ public interface IExchangeRateProvider /// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide /// some of the currencies, ignore them. /// - Task> GetExchangeRates(IEnumerable currencies); + IEnumerable GetExchangeRates(IEnumerable currencies); } diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Providers/ExchangeRateProviderTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Providers/ExchangeRateProviderTests.cs new file mode 100644 index 000000000..c767bef19 --- /dev/null +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Providers/ExchangeRateProviderTests.cs @@ -0,0 +1,116 @@ +using Castle.Core.Logging; +using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Providers; +using Microsoft.Extensions.Logging; + + +namespace ExchangeRateUpdater.Tests.Providers +{ + [TestFixture] + public class ExchangeRateProviderTests + { + private Mock _exchangeRateServiceMock; + private Mock> _loggerMock; + private ExchangeRateProvider _exchangeRateProvider; + + [SetUp] + public void SetUp() + { + _exchangeRateServiceMock = new Mock(); + _exchangeRateServiceMock.Setup(_exchangeRateServiceMock => _exchangeRateServiceMock.GetExchangeRatesAsync()) + .ReturnsAsync(new ExchangeRatesDTO + { + Rates = new List + { + new () + { + CurrencyCode = "ABC", + Rate = 1.0M, + Amount = 1 + }, + new () + { + CurrencyCode = "DEF", + Rate = 30M, + Amount = 10 + }, + new () + { + CurrencyCode = "GHI", + Rate = 20M, + Amount = 100 + } + } + }); + _loggerMock = new Mock>(); + _exchangeRateProvider = new ExchangeRateProvider(_exchangeRateServiceMock.Object, _loggerMock.Object); + } + + [Test] + public void GetExchangeRates_ShouldReturnCorrectExchangeRates() + { + // Arrange + var currencies = new List + { + new("ABC"), + new("DEF"), + new("GHI") + }; + var expected = new List + { + new(new Currency("ABC"), new Currency("CZK"), 1M), + new(new Currency("DEF"), new Currency("CZK"), 3M), + new(new Currency("GHI"), new Currency("CZK"), 0.2M) + }; + + // Act + var actual = _exchangeRateProvider.GetExchangeRates(currencies); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void GetExchangeRates_ShouldReturnAnEmptyList_WhenNoMatchingCurrencies() + { + // Arrange + var currencies = new List + { + new("RST"), + new("UVW"), + new("XYZ") + }; + + // Act + var actual = _exchangeRateProvider.GetExchangeRates(currencies); + + // Assert + actual.Should().BeEmpty(); + } + + [Test] + public void GetExchangeRates_ShouldLogInformation_WhenNoMatchingCurrencies() + { + // Arrange + var currencies = new List + { + new("JKL"), + new("MNO"), + new("PQR") + }; + + // Act + _exchangeRateProvider.GetExchangeRates(currencies); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("No matching rates found for chosen currency codes")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + } +} From 7affe4c114003e49bae50fad1bb3315887646749 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Tue, 14 Jan 2025 13:18:22 +0000 Subject: [PATCH 26/35] Minor: Updated service, refactored return, added logging, caching only when results returned, added tests --- jobs/Backend/Task/ExchangeRateUpdater.csproj | 1 + .../Task/Services/ExchangeRateService.cs | 38 ++++- .../Services/ExchangeRateServiceException.cs | 10 ++ .../Task/Services/IExchangeRateService.cs | 9 +- .../ExchangeRateUpdater.Tests.csproj | 1 + .../GlobalUsings.cs | 6 +- .../Services/ExchangeRateServiceTests.cs | 156 +++++++++++++++--- 7 files changed, 187 insertions(+), 34 deletions(-) create mode 100644 jobs/Backend/Task/Services/ExchangeRateServiceException.cs diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 6b31352da..09e723638 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -6,6 +6,7 @@ + diff --git a/jobs/Backend/Task/Services/ExchangeRateService.cs b/jobs/Backend/Task/Services/ExchangeRateService.cs index 7d73ea578..5ae09cba2 100644 --- a/jobs/Backend/Task/Services/ExchangeRateService.cs +++ b/jobs/Backend/Task/Services/ExchangeRateService.cs @@ -3,6 +3,9 @@ using System.Threading.Tasks; using System; using System.Net.Http.Json; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using System.Linq; namespace ExchangeRateUpdater.Services; @@ -10,24 +13,43 @@ public class ExchangeRateService: IExchangeRateService { private readonly HttpClient _httpClient; private readonly TimeProvider _timeProvider; + private readonly IMemoryCache _cache; + private readonly ILogger _logger; - public ExchangeRateService(HttpClient httpClient, TimeProvider timeProvider) + public ExchangeRateService(HttpClient httpClient, + TimeProvider timeProvider, + IMemoryCache memoryCache, + ILogger logger) { - _httpClient = httpClient; + _httpClient = httpClient; _timeProvider = timeProvider; - } + _cache = memoryCache; + _logger = logger; + } - public async Task GetExchangeRates() + /// + public async Task GetExchangeRatesAsync() { - var todaysDate = _timeProvider.GetUtcNow().ToString("yyyy-MM-dd"); - var response = await _httpClient.GetAsync($"exrates/daily?date={todaysDate}"); + if (_cache.TryGetValue("ExchangeRates", out ExchangeRatesDTO result)) + { + _logger.LogInformation("Exchange rates retrieved from cache"); + return result; + } + + var response = await _httpClient.GetAsync($"exrates/daily?date={_timeProvider.GetUtcNow():yyyy-MM-dd}"); if (!response.IsSuccessStatusCode) { - throw new Exception($"Failed to get exchange rates. Status code: {response.StatusCode}"); + _logger.LogError("Failed to get exchange rates. Status code: {statusCode}", response.StatusCode); + throw new ExchangeRateServiceException($"Failed to get exchange rates. Status code: {response.StatusCode}"); } - var result = await response.Content.ReadFromJsonAsync(); + result = await response.Content.ReadFromJsonAsync(); + + if (result.Rates.Any()) + { + _cache.Set("ExchangeRates", result, TimeSpan.FromSeconds(60)); + } return result; } diff --git a/jobs/Backend/Task/Services/ExchangeRateServiceException.cs b/jobs/Backend/Task/Services/ExchangeRateServiceException.cs new file mode 100644 index 000000000..8dbab5df4 --- /dev/null +++ b/jobs/Backend/Task/Services/ExchangeRateServiceException.cs @@ -0,0 +1,10 @@ +using System; + +namespace ExchangeRateUpdater.Services +{ + public class ExchangeRateServiceException : Exception + { + public ExchangeRateServiceException(string message) : base(message) { } + + } +} diff --git a/jobs/Backend/Task/Services/IExchangeRateService.cs b/jobs/Backend/Task/Services/IExchangeRateService.cs index a53ee600b..cfc329d56 100644 --- a/jobs/Backend/Task/Services/IExchangeRateService.cs +++ b/jobs/Backend/Task/Services/IExchangeRateService.cs @@ -5,5 +5,12 @@ namespace ExchangeRateUpdater.Services; public interface IExchangeRateService { - Task GetExchangeRates(); + /// + /// Retrieves the exchange rates asynchronously. + /// If the exchange rates are available in the cache, it returns the cached rates. + /// Otherwise, calls an external api, caches the result, and returns it. + /// + /// A task result containing the exchange rates. + /// Thrown when the HTTP request to get exchange rates fails. + Task GetExchangeRatesAsync(); } diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj b/jobs/Backend/TestExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj index 8cdde4199..0eb963de4 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/GlobalUsings.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/GlobalUsings.cs index cefced496..1c017df7e 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/GlobalUsings.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/GlobalUsings.cs @@ -1 +1,5 @@ -global using NUnit.Framework; \ No newline at end of file +global using NUnit.Framework; +global using ExchangeRateUpdater.DTOs; +global using ExchangeRateUpdater.Services; +global using FluentAssertions; +global using Moq; \ No newline at end of file diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs index 34c72f89e..0320cf3bc 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Services/ExchangeRateServiceTests.cs @@ -1,6 +1,6 @@ -using ExchangeRateUpdater.DTOs; -using ExchangeRateUpdater.Services; -using Moq; +using FluentAssertions.Execution; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; using Moq.Protected; using System.Net; @@ -9,12 +9,23 @@ namespace TestExchangeRateUpdater.Tests.Services; [TestFixture] public class ExchangeRateServiceTests { + private MemoryCache _memoryCache; + private Mock> _loggerMock; private ExchangeRateService _exchangeRateService; private Mock _timeProviderMock; [SetUp] public void Setup() { + _timeProviderMock = new Mock(); + _timeProviderMock + .Setup(x => x.GetUtcNow()) + .Returns(new DateTime(2025, 1, 11)); + + _memoryCache = new MemoryCache(new MemoryCacheOptions()); + + _loggerMock = new Mock>(); + var handlerMock = new Mock(); handlerMock .Protected() @@ -25,21 +36,29 @@ public void Setup() .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, - Content = new StringContent - ( - "{\"rates\": [{\"validFor\": \"2025-01-11\", \"order\": 1, \"currency\": \"Currency\", \"country\": \"Country\", \"amount\": 1, \"currencyCode\": \"CUR\", \"rate\": 10.00}]}" - ) + Content = new StringContent("{\"rates\": [{\"validFor\": \"2025-01-11\", \"order\": 1, \"currency\": \"Currency\", \"country\": \"Country\", \"amount\": 1, \"currencyCode\": \"CUR\", \"rate\": 10.00}]}") }); - var httpClient = new HttpClient(handlerMock.Object); - httpClient.BaseAddress = new Uri("https://baseaddress/"); - _timeProviderMock = new Mock(); - _timeProviderMock.Setup(x => x.GetUtcNow()).Returns(new DateTime(2025, 1, 11)); - _exchangeRateService = new ExchangeRateService(httpClient, _timeProviderMock.Object); + var httpClient = new HttpClient(handlerMock.Object) + { + BaseAddress = new Uri("https://baseaddress/") + }; + + _exchangeRateService = new ExchangeRateService(httpClient, + _timeProviderMock.Object, + _memoryCache, + _loggerMock.Object); + + } + + [TearDown] + public void TearDown() + { + _memoryCache.Dispose(); } [Test] - public void GetExchangeRates_ShouldReturn_CorrectExchangeRatesDTO() + public void GetExchangeRatesAsync_ShouldReturnCorrectExchangeRatesDTO_AndSetCache() { // Arrange var expected = new ExchangeRatesDTO @@ -56,14 +75,20 @@ public void GetExchangeRates_ShouldReturn_CorrectExchangeRatesDTO() }; // Act - var actual = _exchangeRateService.GetExchangeRates().Result; + var actual = _exchangeRateService.GetExchangeRatesAsync().Result; // Assert - Assert.That(actual.Rates, Is.EqualTo(expected.Rates)); + using (new AssertionScope()) + { + actual.Should().BeEquivalentTo(expected); + + var cachedRates = _memoryCache.Get("ExchangeRates"); + cachedRates.Should().BeEquivalentTo(expected); + } } [Test] - public void GetExchangeRates_ShouldThrowException_WhenResponseIsNotSuccessful() + public void GetExchangeRatesAsync_ShouldNotSetCache_WhenResponseIsSuccessfullButReturnsNoRates() { // Arrange var handlerMock = new Mock(); @@ -75,18 +100,101 @@ public void GetExchangeRates_ShouldThrowException_WhenResponseIsNotSuccessful() ItExpr.IsAny()) .ReturnsAsync(new HttpResponseMessage { - StatusCode = HttpStatusCode.BadRequest + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"rates\": []}") }); - var httpClient = new HttpClient(handlerMock.Object); - httpClient.BaseAddress = new Uri("https://baseaddress/"); + var httpClient = new HttpClient(handlerMock.Object) + { + BaseAddress = new Uri("https://baseaddress/") + }; - _exchangeRateService = new ExchangeRateService(httpClient, _timeProviderMock.Object); + _exchangeRateService = new ExchangeRateService(httpClient, + _timeProviderMock.Object, + _memoryCache, + _loggerMock.Object); + + // Act + _ = _exchangeRateService.GetExchangeRatesAsync().Result; // Assert - Assert.Multiple(() => + _memoryCache.TryGetValue("ExchangeRates", out ExchangeRatesDTO result).Should().BeFalse(); + } + + [Test] + public void GetExchangeRatesAsync_ShouldReturnCachedExchangeRatesDTO_AndLogInformation_WhenCacheIsNotEmpty() + { + // Arrange + var expected = new ExchangeRatesDTO + { + Rates = new List + { + new () + { + Amount = 1, + CurrencyCode = "XXX", + Rate = 20.00M + } + } + }; + + _memoryCache.Set("ExchangeRates", expected); + + // Act + var actual = _exchangeRateService.GetExchangeRatesAsync().Result; + + // Assert + using (new AssertionScope()) + { + actual.Should().BeEquivalentTo(expected); + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("Exchange rates retrieved from cache")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + } + + [Test] + public void GetExchangeRatesAsync_ShouldThrowException_WhenResponseIsNotSuccessful() + { + // Arrange + var handlerMock = new Mock(); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.BadRequest + }); + var httpClient = new HttpClient(handlerMock.Object) + { + BaseAddress = new Uri("https://baseaddress/") + }; + + _exchangeRateService = new ExchangeRateService(httpClient, + _timeProviderMock.Object, + _memoryCache, + _loggerMock.Object); + + // Act and Assert + using (new AssertionScope()) { - var exception = Assert.ThrowsAsync(() => _exchangeRateService.GetExchangeRates()); - Assert.That(exception.Message, Is.EqualTo("Failed to get exchange rates. Status code: BadRequest")); - }); + var exception = Assert.ThrowsAsync(() => _exchangeRateService.GetExchangeRatesAsync()); + exception.Message.Should().Be("Failed to get exchange rates. Status code: BadRequest"); + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("Failed to get exchange rates. Status code: BadRequest")), + null, + It.Is>((v, t) => true)), + Times.Once); + } } } From bae7401463bcd6a08fd8f9c25f7091ad987ff21a Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Tue, 14 Jan 2025 13:18:53 +0000 Subject: [PATCH 27/35] Minor: Program.cs using new classes and returning more complete messages, now runs and refreshes on key press --- jobs/Backend/Task/Program.cs | 61 ++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 0e97c8ede..e5acb9e5a 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,6 +1,7 @@ -using ExchangeRateUpdater; -using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Providers; using ExchangeRateUpdater.Services; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -17,6 +18,7 @@ }); services.AddTransient(); services.AddSingleton(TimeProvider.System); + services.AddSingleton(); services.AddLogging(); }) .ConfigureLogging(logging => @@ -40,19 +42,44 @@ new Currency("XYZ") }; -try -{ - var rates = exchangeRateProvider.GetExchangeRates(currencies).Result; - - Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); - foreach (var rate in rates) - { - Console.WriteLine(rate.ToString()); - } -} -catch (Exception e) -{ - Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); +while (true) +{ + try + { + var rates = exchangeRateProvider.GetExchangeRates(currencies); + + if (!rates.Any()) + { + Console.WriteLine(string.Empty); + Console.WriteLine("No exchange rates found for the chosen currencies."); + Console.WriteLine(string.Empty); + Console.WriteLine("Press any key to retry..."); + Console.ReadKey(); + Console.Clear(); + continue; + } + + Console.WriteLine(string.Empty); + Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); + Console.WriteLine(string.Empty); + + foreach (var rate in rates) + { + Console.WriteLine(rate.ToString()); + Console.WriteLine(string.Empty); + } + + Console.WriteLine("Press any key to refresh..."); + Console.ReadKey(); + Console.Clear(); + } + catch (Exception e) + { + Console.WriteLine(string.Empty); + Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); + Console.WriteLine(string.Empty); + Console.WriteLine("Press any key to retry..."); + Console.ReadKey(); + Console.Clear(); + } } - -Console.ReadLine(); \ No newline at end of file From 1f9fc728827893fbe33641218c96d351f16cd54a Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Tue, 14 Jan 2025 13:22:59 +0000 Subject: [PATCH 28/35] Minor: Added better variable names to linq query --- jobs/Backend/Task/Providers/ExchangeRateProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jobs/Backend/Task/Providers/ExchangeRateProvider.cs b/jobs/Backend/Task/Providers/ExchangeRateProvider.cs index bbd8f56b2..6694c6f37 100644 --- a/jobs/Backend/Task/Providers/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/Providers/ExchangeRateProvider.cs @@ -27,8 +27,8 @@ public IEnumerable GetExchangeRates(IEnumerable currenci var exchangeRates = _exchangeRateService.GetExchangeRatesAsync().Result; var filteredRates = exchangeRates.Rates - .Where(e => - currencies.Any(c => c.Code == e.CurrencyCode)) + .Where(rate => + currencies.Any(currency => currency.Code == rate.CurrencyCode)) .Select(rate => new ExchangeRate(new Currency(rate.CurrencyCode), new Currency("CZK"), rate.Rate / rate.Amount)); From d3eca97a0a2d83acf702dc39d2823075a7ac48bd Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Wed, 15 Jan 2025 11:55:30 +0000 Subject: [PATCH 29/35] Minor: Fixed blank line --- jobs/Backend/Task/Services/ExchangeRateServiceException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jobs/Backend/Task/Services/ExchangeRateServiceException.cs b/jobs/Backend/Task/Services/ExchangeRateServiceException.cs index 8dbab5df4..19b079278 100644 --- a/jobs/Backend/Task/Services/ExchangeRateServiceException.cs +++ b/jobs/Backend/Task/Services/ExchangeRateServiceException.cs @@ -4,7 +4,6 @@ namespace ExchangeRateUpdater.Services { public class ExchangeRateServiceException : Exception { - public ExchangeRateServiceException(string message) : base(message) { } - + public ExchangeRateServiceException(string message) : base(message) { } } } From 17549c2e4d20c68b73d6b962ef12c144fe234074 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Wed, 15 Jan 2025 19:05:53 +0000 Subject: [PATCH 30/35] Minor: Added Readme, updated remaining files to filescoped namespaces, fixed tests now that Provider is async again, added resiliance --- jobs/Backend/Task/Program.cs | 5 +- .../Task/Providers/ExchangeRateProvider.cs | 4 +- .../Task/Providers/IExchangeRateProvider.cs | 2 +- jobs/Backend/Task/Readme.md | 30 +++ .../Services/ExchangeRateServiceException.cs | 9 +- .../Providers/ExchangeRateProviderTests.cs | 181 +++++++++--------- 6 files changed, 130 insertions(+), 101 deletions(-) create mode 100644 jobs/Backend/Task/Readme.md diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index e5acb9e5a..710f7a0a0 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -15,7 +15,8 @@ client => { client.BaseAddress = new Uri("https://api.cnb.cz/cnbapi/"); - }); + }) + .AddStandardResilienceHandler(); services.AddTransient(); services.AddSingleton(TimeProvider.System); services.AddSingleton(); @@ -46,7 +47,7 @@ { try { - var rates = exchangeRateProvider.GetExchangeRates(currencies); + var rates = await exchangeRateProvider.GetExchangeRates(currencies); if (!rates.Any()) { diff --git a/jobs/Backend/Task/Providers/ExchangeRateProvider.cs b/jobs/Backend/Task/Providers/ExchangeRateProvider.cs index 6694c6f37..8484094ed 100644 --- a/jobs/Backend/Task/Providers/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/Providers/ExchangeRateProvider.cs @@ -22,9 +22,9 @@ public ExchangeRateProvider(IExchangeRateService exchangeRateService, ILogger - public IEnumerable GetExchangeRates(IEnumerable currencies) + public async Task> GetExchangeRates(IEnumerable currencies) { - var exchangeRates = _exchangeRateService.GetExchangeRatesAsync().Result; + var exchangeRates = await _exchangeRateService.GetExchangeRatesAsync(); var filteredRates = exchangeRates.Rates .Where(rate => diff --git a/jobs/Backend/Task/Providers/IExchangeRateProvider.cs b/jobs/Backend/Task/Providers/IExchangeRateProvider.cs index fecfed3ab..36e6c76e4 100644 --- a/jobs/Backend/Task/Providers/IExchangeRateProvider.cs +++ b/jobs/Backend/Task/Providers/IExchangeRateProvider.cs @@ -11,5 +11,5 @@ public interface IExchangeRateProvider /// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide /// some of the currencies, ignore them. /// - IEnumerable GetExchangeRates(IEnumerable currencies); + Task> GetExchangeRates(IEnumerable currencies); } diff --git a/jobs/Backend/Task/Readme.md b/jobs/Backend/Task/Readme.md new file mode 100644 index 000000000..00618822c --- /dev/null +++ b/jobs/Backend/Task/Readme.md @@ -0,0 +1,30 @@ +# Task + +This PR addresses the requirement to implement an exchange rate provider for the Czech National Bank, using their publically available APIs to source +the exchange rate data for specifically requested currencies. The currencies are to be returned to the user in a console application. + +## Approach + +I have approached this task with the idea of creating an MVP that satisfies the requirements. That means that I have retained the console application, and extended +it's functionality to return the required data. My main focus was to keep the code clean, clear and SOLID so as to be easily maintainable and extensible. I had +considered replacing this console application with a minimal api for example, but in the end decided that my focus here should be on delivering the requirementsas as +written. I expect that this program would eventually service a number of front ends and at that point, adding an api would be more appropriate. + +I have implemented logging and caching - these are simple implementations appropriate for a console application. For logging, I have kept log messages clear and simple, +with only one log instance requiring structued logging. These logs are simple console logs. In a production app, I would expect to connect to a provider such as +application insights. Similarly, for the caching that is a simple in memory cache - it would be preferable to implement a seperate caching source running independantly +from the main application, such as Redis for example, to increase the resiliance of the cache. I have set a short time to live for the purposes of this test, but in a +real world solution would probably look to refresh the cache inline with source API. + +I have added unit tests for updated Provider and Service classes. For the sake of brevity I have used multiple asserts in each test scenario, as I believe that keeps test +code clearer and easier to read, without sacrificing testing the classes logic. + +More generally, I have updated the project to .net 8, and brought the program.cs file in line with modern coding approaches. I have used global usings to reduce 'noise' +in files, and have used file scoped namespacing to make the code easier to read. I have used IHttpClientFactory to create the HttpClients and typed them, as well as +added standard resiliance handling to deal with failed calls. + +## Improvements + +- Implement external services for logging and caching +- Accept user input to search for specific currency or a list of currencies, for specific dates +- Provide an API diff --git a/jobs/Backend/Task/Services/ExchangeRateServiceException.cs b/jobs/Backend/Task/Services/ExchangeRateServiceException.cs index 19b079278..66661b835 100644 --- a/jobs/Backend/Task/Services/ExchangeRateServiceException.cs +++ b/jobs/Backend/Task/Services/ExchangeRateServiceException.cs @@ -1,9 +1,8 @@ using System; -namespace ExchangeRateUpdater.Services +namespace ExchangeRateUpdater.Services; + +public class ExchangeRateServiceException : Exception { - public class ExchangeRateServiceException : Exception - { - public ExchangeRateServiceException(string message) : base(message) { } - } + public ExchangeRateServiceException(string message) : base(message) { } } diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/Providers/ExchangeRateProviderTests.cs b/jobs/Backend/TestExchangeRateUpdater.Tests/Providers/ExchangeRateProviderTests.cs index c767bef19..f64e26d77 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/Providers/ExchangeRateProviderTests.cs +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/Providers/ExchangeRateProviderTests.cs @@ -4,113 +4,112 @@ using Microsoft.Extensions.Logging; -namespace ExchangeRateUpdater.Tests.Providers +namespace ExchangeRateUpdater.Tests.Providers; + +[TestFixture] +public class ExchangeRateProviderTests { - [TestFixture] - public class ExchangeRateProviderTests - { - private Mock _exchangeRateServiceMock; - private Mock> _loggerMock; - private ExchangeRateProvider _exchangeRateProvider; + private Mock _exchangeRateServiceMock; + private Mock> _loggerMock; + private ExchangeRateProvider _exchangeRateProvider; - [SetUp] - public void SetUp() - { - _exchangeRateServiceMock = new Mock(); - _exchangeRateServiceMock.Setup(_exchangeRateServiceMock => _exchangeRateServiceMock.GetExchangeRatesAsync()) - .ReturnsAsync(new ExchangeRatesDTO + [SetUp] + public void SetUp() + { + _exchangeRateServiceMock = new Mock(); + _exchangeRateServiceMock.Setup(_exchangeRateServiceMock => _exchangeRateServiceMock.GetExchangeRatesAsync()) + .ReturnsAsync(new ExchangeRatesDTO + { + Rates = new List { - Rates = new List + new () { - new () - { - CurrencyCode = "ABC", - Rate = 1.0M, - Amount = 1 - }, - new () - { - CurrencyCode = "DEF", - Rate = 30M, - Amount = 10 - }, - new () - { - CurrencyCode = "GHI", - Rate = 20M, - Amount = 100 - } + CurrencyCode = "ABC", + Rate = 1.0M, + Amount = 1 + }, + new () + { + CurrencyCode = "DEF", + Rate = 30M, + Amount = 10 + }, + new () + { + CurrencyCode = "GHI", + Rate = 20M, + Amount = 100 } - }); - _loggerMock = new Mock>(); - _exchangeRateProvider = new ExchangeRateProvider(_exchangeRateServiceMock.Object, _loggerMock.Object); - } + } + }); + _loggerMock = new Mock>(); + _exchangeRateProvider = new ExchangeRateProvider(_exchangeRateServiceMock.Object, _loggerMock.Object); + } - [Test] - public void GetExchangeRates_ShouldReturnCorrectExchangeRates() + [Test] + public void GetExchangeRates_ShouldReturnCorrectExchangeRates() + { + // Arrange + var currencies = new List { - // Arrange - var currencies = new List - { - new("ABC"), - new("DEF"), - new("GHI") - }; - var expected = new List - { - new(new Currency("ABC"), new Currency("CZK"), 1M), - new(new Currency("DEF"), new Currency("CZK"), 3M), - new(new Currency("GHI"), new Currency("CZK"), 0.2M) - }; + new("ABC"), + new("DEF"), + new("GHI") + }; + var expected = new List + { + new(new Currency("ABC"), new Currency("CZK"), 1M), + new(new Currency("DEF"), new Currency("CZK"), 3M), + new(new Currency("GHI"), new Currency("CZK"), 0.2M) + }; - // Act - var actual = _exchangeRateProvider.GetExchangeRates(currencies); + // Act + var actual = _exchangeRateProvider.GetExchangeRates(currencies).Result; - // Assert - actual.Should().BeEquivalentTo(expected); - } + // Assert + actual.Should().BeEquivalentTo(expected); + } - [Test] - public void GetExchangeRates_ShouldReturnAnEmptyList_WhenNoMatchingCurrencies() + [Test] + public void GetExchangeRates_ShouldReturnAnEmptyList_WhenNoMatchingCurrencies() + { + // Arrange + var currencies = new List { - // Arrange - var currencies = new List - { - new("RST"), - new("UVW"), - new("XYZ") - }; + new("RST"), + new("UVW"), + new("XYZ") + }; - // Act - var actual = _exchangeRateProvider.GetExchangeRates(currencies); + // Act + var actual = _exchangeRateProvider.GetExchangeRates(currencies).Result; - // Assert - actual.Should().BeEmpty(); - } + // Assert + actual.Should().BeEmpty(); + } - [Test] - public void GetExchangeRates_ShouldLogInformation_WhenNoMatchingCurrencies() + [Test] + public void GetExchangeRates_ShouldLogInformation_WhenNoMatchingCurrencies() + { + // Arrange + var currencies = new List { - // Arrange - var currencies = new List - { - new("JKL"), - new("MNO"), - new("PQR") - }; + new("JKL"), + new("MNO"), + new("PQR") + }; - // Act - _exchangeRateProvider.GetExchangeRates(currencies); + // Act + _ = _exchangeRateProvider.GetExchangeRates(currencies); - // Assert - _loggerMock.Verify( - x => x.Log( - LogLevel.Information, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("No matching rates found for chosen currency codes")), - It.IsAny(), - It.Is>((v, t) => true)), - Times.Once); - } + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("No matching rates found for chosen currency codes")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); } } From 2d226869aa6843df42ec626f4adecb6df80626fd Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Wed, 15 Jan 2025 19:07:16 +0000 Subject: [PATCH 31/35] Minor: Package updates --- jobs/Backend/Task/ExchangeRateUpdater.csproj | 8 ++++---- .../ExchangeRateUpdater.Tests.csproj | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 09e723638..4e20e7321 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -6,10 +6,10 @@ - - - - + + + + \ No newline at end of file diff --git a/jobs/Backend/TestExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj b/jobs/Backend/TestExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj index 0eb963de4..1b8436626 100644 --- a/jobs/Backend/TestExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj +++ b/jobs/Backend/TestExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj @@ -11,7 +11,7 @@ - + From e3a59d487f0a9fb118898413d82316dc61719d25 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Wed, 15 Jan 2025 19:34:59 +0000 Subject: [PATCH 32/35] Minor: Updated Readme to include instructions on running the application --- jobs/Backend/Task/Readme.md | 49 ++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/jobs/Backend/Task/Readme.md b/jobs/Backend/Task/Readme.md index 00618822c..d6ed9707e 100644 --- a/jobs/Backend/Task/Readme.md +++ b/jobs/Backend/Task/Readme.md @@ -1,30 +1,35 @@ -# Task +## Exchange Rate Updater Readme -This PR addresses the requirement to implement an exchange rate provider for the Czech National Bank, using their publically available APIs to source -the exchange rate data for specifically requested currencies. The currencies are to be returned to the user in a console application. +# Description -## Approach +This console application provides today's exchange rates for a given set of currencies. Once running, the application will persist until the user closes it. -I have approached this task with the idea of creating an MVP that satisfies the requirements. That means that I have retained the console application, and extended -it's functionality to return the required data. My main focus was to keep the code clean, clear and SOLID so as to be easily maintainable and extensible. I had -considered replacing this console application with a minimal api for example, but in the end decided that my focus here should be on delivering the requirementsas as -written. I expect that this program would eventually service a number of front ends and at that point, adding an api would be more appropriate. +Rates can be refreshed by pressing any key, and failed requests can be resent by pressing any key. The application will advise if the displayed rates are from the API or are cached. -I have implemented logging and caching - these are simple implementations appropriate for a console application. For logging, I have kept log messages clear and simple, -with only one log instance requiring structued logging. These logs are simple console logs. In a production app, I would expect to connect to a provider such as -application insights. Similarly, for the caching that is a simple in memory cache - it would be preferable to implement a seperate caching source running independantly -from the main application, such as Redis for example, to increase the resiliance of the cache. I have set a short time to live for the purposes of this test, but in a -real world solution would probably look to refresh the cache inline with source API. +## Requirements -I have added unit tests for updated Provider and Service classes. For the sake of brevity I have used multiple asserts in each test scenario, as I believe that keeps test -code clearer and easier to read, without sacrificing testing the classes logic. +- Ensure the .NET 8 SDK is installed -More generally, I have updated the project to .net 8, and brought the program.cs file in line with modern coding approaches. I have used global usings to reduce 'noise' -in files, and have used file scoped namespacing to make the code easier to read. I have used IHttpClientFactory to create the HttpClients and typed them, as well as -added standard resiliance handling to deal with failed calls. +## Instructions -## Improvements +To run the app, either open the solution in Visual Studio and execute by pressing F5. -- Implement external services for logging and caching -- Accept user input to search for specific currency or a list of currencies, for specific dates -- Provide an API +Alternatively, navigate to the folder containing the application and execute + +```sh +dotnet restore +``` + +```sh +dotnet build +``` + +```sh +dotnet run +``` + +To run the unit tests + +```sh +dotnet test +``` \ No newline at end of file From 3332e32b39c5b142feaa1c34ea1e4d59c3309db4 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Wed, 15 Jan 2025 19:37:18 +0000 Subject: [PATCH 33/35] Minor: Removed restore step from instructions --- jobs/Backend/Task/Readme.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/jobs/Backend/Task/Readme.md b/jobs/Backend/Task/Readme.md index d6ed9707e..81d00ca50 100644 --- a/jobs/Backend/Task/Readme.md +++ b/jobs/Backend/Task/Readme.md @@ -16,10 +16,6 @@ To run the app, either open the solution in Visual Studio and execute by pressin Alternatively, navigate to the folder containing the application and execute -```sh -dotnet restore -``` - ```sh dotnet build ``` From 77272cd68619ee067097869c4722ca8344069a23 Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Wed, 15 Jan 2025 19:37:58 +0000 Subject: [PATCH 34/35] Minor: Formatting changes for readme --- jobs/Backend/Task/Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jobs/Backend/Task/Readme.md b/jobs/Backend/Task/Readme.md index 81d00ca50..8fb323aa5 100644 --- a/jobs/Backend/Task/Readme.md +++ b/jobs/Backend/Task/Readme.md @@ -14,7 +14,7 @@ Rates can be refreshed by pressing any key, and failed requests can be resent by To run the app, either open the solution in Visual Studio and execute by pressing F5. -Alternatively, navigate to the folder containing the application and execute +Alternatively, navigate to the folder containing the application and execute: ```sh dotnet build @@ -24,7 +24,7 @@ dotnet build dotnet run ``` -To run the unit tests +To run the unit tests: ```sh dotnet test From f6d6712ca9418faa0ab40b57d286fd7edb8f599f Mon Sep 17 00:00:00 2001 From: Graeme Hedges Date: Wed, 15 Jan 2025 19:39:07 +0000 Subject: [PATCH 35/35] Minor: Fixed readme titles --- jobs/Backend/Task/Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jobs/Backend/Task/Readme.md b/jobs/Backend/Task/Readme.md index 8fb323aa5..64e78a96b 100644 --- a/jobs/Backend/Task/Readme.md +++ b/jobs/Backend/Task/Readme.md @@ -1,6 +1,6 @@ -## Exchange Rate Updater Readme +# Exchange Rate Updater Readme -# Description +## Description This console application provides today's exchange rates for a given set of currencies. Once running, the application will persist until the user closes it.