From ccb726b1bce760bf89492e07ce2798a6083e2847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Banzas=20Bar=C3=B3?= Date: Wed, 19 Jun 2024 14:29:08 +0200 Subject: [PATCH 1/4] Added basic functionality --- .gitignore | 2 ++ jobs/Backend/Task/API/CNB/CNBApiClient.cs | 31 +++++++++++++++++++ .../Model/Responses/ExRateDailyResponse.cs | 9 ++++++ .../CNB/Model/Responses/ExRateDailyRest.cs | 15 +++++++++ jobs/Backend/Task/ExchangeRateProvider.cs | 15 +++++++-- jobs/Backend/Task/Program.cs | 5 +-- 6 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 jobs/Backend/Task/API/CNB/CNBApiClient.cs create mode 100644 jobs/Backend/Task/API/CNB/Model/Responses/ExRateDailyResponse.cs create mode 100644 jobs/Backend/Task/API/CNB/Model/Responses/ExRateDailyRest.cs diff --git a/.gitignore b/.gitignore index fd3586545..51164418b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ node_modules bower_components npm-debug.log +*.vsidx +*.v2 diff --git a/jobs/Backend/Task/API/CNB/CNBApiClient.cs b/jobs/Backend/Task/API/CNB/CNBApiClient.cs new file mode 100644 index 000000000..2a44a8d3d --- /dev/null +++ b/jobs/Backend/Task/API/CNB/CNBApiClient.cs @@ -0,0 +1,31 @@ + +using ExchangeRateUpdater.API.CNB.Model.Responses; +using System.Net.Http; +using System.Threading.Tasks; +using System.Net.Http.Json; +using System; + +namespace ExchangeRateUpdater.API.CNB +{ + public static class CNBApiClient + { + private const string API_URL = "https://api.cnb.cz/cnbapi"; + private const string EX_RATES = "exrates"; + private const string DAILY_RATES = "daily"; + public static async Task GetDailyRates(string lang = "EN", DateTime? date = null) + { + using var hc = new HttpClient(); + var query = DAILY_RATES + $"?lang={lang}"; + + if (date.HasValue) + query += $"date={date.Value:yyyy-MM-dd}"; + + var response = await hc.GetAsync(string.Join("/", API_URL, EX_RATES, query)); + + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadFromJsonAsync(); + } + + } +} diff --git a/jobs/Backend/Task/API/CNB/Model/Responses/ExRateDailyResponse.cs b/jobs/Backend/Task/API/CNB/Model/Responses/ExRateDailyResponse.cs new file mode 100644 index 000000000..d64cc4756 --- /dev/null +++ b/jobs/Backend/Task/API/CNB/Model/Responses/ExRateDailyResponse.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace ExchangeRateUpdater.API.CNB.Model.Responses +{ + public class ExRateDailyResponse + { + public IEnumerable Rates { get; set; } + } +} diff --git a/jobs/Backend/Task/API/CNB/Model/Responses/ExRateDailyRest.cs b/jobs/Backend/Task/API/CNB/Model/Responses/ExRateDailyRest.cs new file mode 100644 index 000000000..ed371d3e5 --- /dev/null +++ b/jobs/Backend/Task/API/CNB/Model/Responses/ExRateDailyRest.cs @@ -0,0 +1,15 @@ +using System; + +namespace ExchangeRateUpdater.API.CNB.Model.Responses +{ + public class ExRateDailyRest + { + public long Amount { get; set; } + public string Country { get; set; } + public string Currency { get; set; } + public string CurrencyCode { get; set; } + public int Order { get; set; } + public decimal Rate { get; set; } + public DateTime ValidFor { get; set; } + } +} diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs index 6f82a97fb..f5198bdf2 100644 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/ExchangeRateProvider.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using ExchangeRateUpdater.API.CNB; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace ExchangeRateUpdater { @@ -11,9 +13,16 @@ public class ExchangeRateProvider /// 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 async Task> GetExchangeRates(IEnumerable currencies) { - return Enumerable.Empty(); + var currencyCodes = currencies.Select(c => c.Code).ToList(); + + var result = await CNBApiClient.GetDailyRates(); + + return result.Rates.Where(r => currencyCodes.Contains(r.CurrencyCode)) + .Select(r => new ExchangeRate(new Currency(r.CurrencyCode), new Currency("CZK"), r.Rate / r.Amount)); } } } diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 379a69b1f..57d45fb5b 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace ExchangeRateUpdater { @@ -19,12 +20,12 @@ public static class Program new Currency("XYZ") }; - public static void Main(string[] args) + public static async Task Main(string[] args) { try { var provider = new ExchangeRateProvider(); - var rates = provider.GetExchangeRates(currencies); + var rates = await provider.GetExchangeRates(currencies); Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); foreach (var rate in rates) From 464846c5f59d5ba45566d064eb4ac0dbd31a7b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Banzas=20Bar=C3=B3?= Date: Wed, 19 Jun 2024 19:29:50 +0200 Subject: [PATCH 2/4] Added unit tests --- .../ExchangeRateProviderTest.cs | 139 ++++++++++++++++++ .../ExchangeRateUpdaterTest.csproj | 24 +++ .../Backend/ExchangeRateUpdaterTest/Usings.cs | 1 + .../v17/TestStore/0/000.testlog | Bin 0 -> 135896 bytes .../v17/TestStore/0/testlog.manifest | Bin 0 -> 24 bytes jobs/Backend/Task/API/CNB/CNBApiClient.cs | 10 +- jobs/Backend/Task/ExchangeRateProvider.cs | 13 +- jobs/Backend/Task/ExchangeRateUpdater.sln | 15 +- jobs/Backend/Task/Program.cs | 6 +- 9 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 jobs/Backend/ExchangeRateUpdaterTest/ExchangeRateProviderTest.cs create mode 100644 jobs/Backend/ExchangeRateUpdaterTest/ExchangeRateUpdaterTest.csproj create mode 100644 jobs/Backend/ExchangeRateUpdaterTest/Usings.cs create mode 100644 jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog create mode 100644 jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/testlog.manifest diff --git a/jobs/Backend/ExchangeRateUpdaterTest/ExchangeRateProviderTest.cs b/jobs/Backend/ExchangeRateUpdaterTest/ExchangeRateProviderTest.cs new file mode 100644 index 000000000..8baa36f11 --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdaterTest/ExchangeRateProviderTest.cs @@ -0,0 +1,139 @@ +using ExchangeRateUpdater; +using ExchangeRateUpdater.API.CNB; +using ExchangeRateUpdater.API.CNB.Model.Responses; +using Moq; + +namespace ExchangeRateUpdaterTest +{ + public class ExchangeRateProviderTest + { + private Mock _cnbApiClientMock; + private ExchangeRateProvider _exchangeRateProvider; + + [SetUp] + public void Setup() + { + _cnbApiClientMock = new Mock(); + _exchangeRateProvider = new ExchangeRateProvider(_cnbApiClientMock.Object); + } + + [Test] + public async Task GetExchangeRates_ShouldReturnRatesForSpecifiedCurrencies() + { + // Arrange + var currencies = new List + { + new Currency("USD"), + new Currency("EUR") + }; + + var apiResponse = new ExRateDailyResponse + { + Rates = new List + { + new ExRateDailyRest { CurrencyCode = "USD", Rate = 25.0m, Amount = 1, }, + new ExRateDailyRest { CurrencyCode = "EUR", Rate = 27.0m, Amount = 1 }, + new ExRateDailyRest { CurrencyCode = "GBP", Rate = 30.0m, Amount = 1 } + } + }; + + _cnbApiClientMock.Setup(c => c.GetDailyRates(It.IsAny(), It.IsAny())).ReturnsAsync(apiResponse); + + // Act + var result = await _exchangeRateProvider.GetExchangeRates(currencies); + + // Assert + Assert.That(result.Count(), Is.EqualTo(2)); + Assert.That(result.Any(r => r.SourceCurrency.Code == "USD"), Is.True); + Assert.That(result.Any(r => r.SourceCurrency.Code == "EUR"), Is.True); + } + + [Test] + public async Task GetExchangeRates_ShouldIgnoreUndefinedCurrencies() + { + // Arrange + var currencies = new List + { + new Currency("USD"), + new Currency("XYZ") + }; + + var apiResponse = new ExRateDailyResponse + { + Rates = new List + { + new ExRateDailyRest { CurrencyCode = "USD", Rate = 25.0m, Amount = 1 } + } + }; + + _cnbApiClientMock.Setup(c => c.GetDailyRates(It.IsAny(), It.IsAny())).ReturnsAsync(apiResponse); + + // Act + var result = await _exchangeRateProvider.GetExchangeRates(currencies); + + // Assert + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.Any(r => r.SourceCurrency.Code == "USD"), Is.True); + Assert.That(result.Any(r => r.SourceCurrency.Code == "XYZ"), Is.False); + } + + [Test] + public async Task GetExchangeRates_ShouldReturnEmptyWhenNoRatesDefined() + { + // Arrange + var currencies = new List + { + new Currency("USD"), + new Currency("EUR") + }; + + var apiResponse = new ExRateDailyResponse + { + Rates = new List() + }; + + _cnbApiClientMock.Setup(c => c.GetDailyRates(It.IsAny(), It.IsAny())).ReturnsAsync(apiResponse); + + // Act + var result = await _exchangeRateProvider.GetExchangeRates(currencies); + + // Assert + Assert.That(result, Is.Empty); + } + + [Test] + public async Task GetExchangeRates_ShouldHandleNullRatesFromApi() + { + // Arrange + var currencies = new List + { + new Currency("USD"), + new Currency("EUR") + }; + + var apiResponse = new ExRateDailyResponse + { + Rates = null + }; + + _cnbApiClientMock.Setup(c => c.GetDailyRates(It.IsAny(), It.IsAny())).ReturnsAsync(apiResponse); + + // Act + var result = await _exchangeRateProvider.GetExchangeRates(currencies); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void GetExchangeRates_ShouldThrowException_WhenApiClientFails() + { + // Arrange + var currencies = new List { new Currency("USD") }; + _cnbApiClientMock.Setup(c => c.GetDailyRates(It.IsAny(), It.IsAny())).ThrowsAsync(new Exception("API failure")); + + // Act & Assert + Assert.ThrowsAsync(() => _exchangeRateProvider.GetExchangeRates(currencies)); + } + } +} \ No newline at end of file diff --git a/jobs/Backend/ExchangeRateUpdaterTest/ExchangeRateUpdaterTest.csproj b/jobs/Backend/ExchangeRateUpdaterTest/ExchangeRateUpdaterTest.csproj new file mode 100644 index 000000000..3b81e31cc --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdaterTest/ExchangeRateUpdaterTest.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + diff --git a/jobs/Backend/ExchangeRateUpdaterTest/Usings.cs b/jobs/Backend/ExchangeRateUpdaterTest/Usings.cs new file mode 100644 index 000000000..cefced496 --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdaterTest/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog new file mode 100644 index 0000000000000000000000000000000000000000..3fd711c629e23316bdb39d3ffb1cf3ada9ebcd72 GIT binary patch literal 135896 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 jm$_ GetDailyRates(string lang = "EN", DateTime? date = null); + } + + public class CNBApiClient : ICNBApiClient { private const string API_URL = "https://api.cnb.cz/cnbapi"; private const string EX_RATES = "exrates"; private const string DAILY_RATES = "daily"; - public static async Task GetDailyRates(string lang = "EN", DateTime? date = null) + public async Task GetDailyRates(string lang = "EN", DateTime? date = null) { using var hc = new HttpClient(); var query = DAILY_RATES + $"?lang={lang}"; @@ -26,6 +31,5 @@ public static async Task GetDailyRates(string lang = "EN", return await response.Content.ReadFromJsonAsync(); } - } } diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs index f5198bdf2..829db25a3 100644 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/ExchangeRateProvider.cs @@ -7,6 +7,11 @@ namespace ExchangeRateUpdater { public class ExchangeRateProvider { + private readonly ICNBApiClient _cnbApiClient; + public ExchangeRateProvider(ICNBApiClient cnbApiClient) + { + _cnbApiClient = cnbApiClient; + } /// /// 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", @@ -14,15 +19,15 @@ public class ExchangeRateProvider /// some of the currencies, ignore them. /// /// - + public async Task> GetExchangeRates(IEnumerable currencies) { var currencyCodes = currencies.Select(c => c.Code).ToList(); - var result = await CNBApiClient.GetDailyRates(); + var result = await _cnbApiClient.GetDailyRates(); - return result.Rates.Where(r => currencyCodes.Contains(r.CurrencyCode)) - .Select(r => new ExchangeRate(new Currency(r.CurrencyCode), new Currency("CZK"), r.Rate / r.Amount)); + return result.Rates?.Where(r => currencyCodes.Contains(r.CurrencyCode)) + .Select(r => new ExchangeRate(new Currency(r.CurrencyCode), new Currency("CZK"), r.Rate / r.Amount)); } } } diff --git a/jobs/Backend/Task/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater.sln index 89be84daf..b495d4916 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.sln +++ b/jobs/Backend/Task/ExchangeRateUpdater.sln @@ -1,9 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35004.147 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdaterTest", "..\ExchangeRateUpdaterTest\ExchangeRateUpdaterTest.csproj", "{F481C6F8-6A36-4803-AF21-C0A6CD26C488}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,8 +17,15 @@ Global {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 + {F481C6F8-6A36-4803-AF21-C0A6CD26C488}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F481C6F8-6A36-4803-AF21-C0A6CD26C488}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F481C6F8-6A36-4803-AF21-C0A6CD26C488}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F481C6F8-6A36-4803-AF21-C0A6CD26C488}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {51DB8E9B-E93F-4E6C-8A82-82669B4DCAE2} + EndGlobalSection EndGlobal diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 57d45fb5b..41b6e7aab 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,4 +1,5 @@ -using System; +using ExchangeRateUpdater.API.CNB; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -24,7 +25,8 @@ public static async Task Main(string[] args) { try { - var provider = new ExchangeRateProvider(); + var cnbApiClient = new CNBApiClient(); + var provider = new ExchangeRateProvider(cnbApiClient); var rates = await provider.GetExchangeRates(currencies); Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); From 8b583739be1681e194d360e5fa10037bcc5a464e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Banzas=20Bar=C3=B3?= Date: Wed, 19 Jun 2024 19:32:19 +0200 Subject: [PATCH 3/4] Delete jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/testlog.manifest --- .../v17/TestStore/0/testlog.manifest | Bin 24 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/testlog.manifest diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/testlog.manifest b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/testlog.manifest deleted file mode 100644 index e92ede29d76aefe079835aeae278da5341f6e15c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24 WcmXR;&-W=QP7PsZKmZO+yIuf90t7Vx From ac5728b4b8359b6264ebc21e640a443da93555b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Banzas=20Bar=C3=B3?= Date: Wed, 19 Jun 2024 19:32:39 +0200 Subject: [PATCH 4/4] Delete jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0 directory --- .../v17/TestStore/0/000.testlog | Bin 135896 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog deleted file mode 100644 index 3fd711c629e23316bdb39d3ffb1cf3ada9ebcd72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135896 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 jm$_