From 499b892f4ae4081fe2409e4ff1329dc6bc545a9a Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 17:52:58 +0100 Subject: [PATCH 01/14] feat: add CnbApi and rates --- jobs/Backend/Task/Cnb/CnbApi.cs | 29 +++++++++++++++++++++++++ jobs/Backend/Task/Cnb/CnbRates.cs | 35 +++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 jobs/Backend/Task/Cnb/CnbApi.cs create mode 100644 jobs/Backend/Task/Cnb/CnbRates.cs diff --git a/jobs/Backend/Task/Cnb/CnbApi.cs b/jobs/Backend/Task/Cnb/CnbApi.cs new file mode 100644 index 0000000000..1261fbd47d --- /dev/null +++ b/jobs/Backend/Task/Cnb/CnbApi.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Cnb +{ + internal interface IBankApi + { + Task> GetTodayExchangeRates(); + } + + internal sealed class BankApi : IBankApi + { + private readonly HttpClient _httpClient; + + public BankApi(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task> GetTodayExchangeRates() + { + var exchangeRatesEndpoint = "https://api.cnb.cz/cnbapi/exrates/daily?lang=EN"; + var response = await _httpClient.GetFromJsonAsync(exchangeRatesEndpoint); + return response.Rates; + } + } +} diff --git a/jobs/Backend/Task/Cnb/CnbRates.cs b/jobs/Backend/Task/Cnb/CnbRates.cs new file mode 100644 index 0000000000..c4b786eed9 --- /dev/null +++ b/jobs/Backend/Task/Cnb/CnbRates.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ExchangeRateUpdater.Cnb +{ + internal class CnbExchangeRates + { + [JsonPropertyName("rates")] + [JsonInclude] + public IEnumerable Rates { get; private set; } + } +/// +/// "validFor": "2019-05-17", + // "ordaaaaer": 94, + // "country": "Austrálie", + // "currency": "dolar", + // "amount": 1, + // "currencyCode": "AUD", + // "rate": 15.858aaa +/// + internal sealed class CnbExchangeRate + { + [JsonPropertyName("currencyCode")] + [JsonInclude] + public string CurrencyCode { get; private set; } + + [JsonPropertyName("currencyCode")] + public int Amount { get; set; } + + [JsonPropertyName("rate")] + public decimal Rate { get; set; } + + public decimal RealRate => Rate / Amount; + } +} From 7817ac52f983eaf28516aa5beb307966373df7c6 Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 17:55:28 +0100 Subject: [PATCH 02/14] feat: override Equals and GetHashCode for Currency.cs --- jobs/Backend/Task/Currency.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/Currency.cs index f375776f25..92265e25e4 100644 --- a/jobs/Backend/Task/Currency.cs +++ b/jobs/Backend/Task/Currency.cs @@ -1,7 +1,8 @@ namespace ExchangeRateUpdater { - public class Currency + internal sealed class Currency { + internal static readonly Currency Czk = new Currency("CZK"); public Currency(string code) { Code = code; @@ -12,6 +13,20 @@ public Currency(string code) /// public string Code { get; } + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) return true; + if (obj is null) return false; + if (obj is not Currency other) return false; + + return Code == other.Code; + } + + public override int GetHashCode() + { + return Code.GetHashCode(); + } + public override string ToString() { return Code; From fc09948464eeafb80d17787cdc46847008d0864c Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 17:56:22 +0100 Subject: [PATCH 03/14] feat: add static init for default CZK currency as a source --- jobs/Backend/Task/ExchangeRate.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/jobs/Backend/Task/ExchangeRate.cs b/jobs/Backend/Task/ExchangeRate.cs index 58c5bb10e0..fbf3de61e4 100644 --- a/jobs/Backend/Task/ExchangeRate.cs +++ b/jobs/Backend/Task/ExchangeRate.cs @@ -1,6 +1,6 @@ namespace ExchangeRateUpdater { - public class ExchangeRate + internal sealed class ExchangeRate { public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value) { @@ -15,6 +15,17 @@ public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal va public decimal Value { get; } + /// + /// Returns Exchange Rate with CZK as a source currency + /// + /// + /// + /// + public static ExchangeRate FromCZK(Currency targetCurrency, decimal value) + { + return new ExchangeRate(Currency.Czk, targetCurrency, value); + } + public override string ToString() { return $"{SourceCurrency}/{TargetCurrency}={Value}"; From 91532e4ec1cc7e354abc16bec16ac1305654da9e Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 18:39:58 +0100 Subject: [PATCH 04/14] fix: correct prop name for cnb rate --- .../Cnb/CnbRatesTests.cs | 0 jobs/Backend/Task/Cnb/CnbApi.cs | 12 ++++++++---- jobs/Backend/Task/Cnb/CnbRates.cs | 19 +++++-------------- 3 files changed, 13 insertions(+), 18 deletions(-) create mode 100644 jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs diff --git a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jobs/Backend/Task/Cnb/CnbApi.cs b/jobs/Backend/Task/Cnb/CnbApi.cs index 1261fbd47d..7a0b7317fd 100644 --- a/jobs/Backend/Task/Cnb/CnbApi.cs +++ b/jobs/Backend/Task/Cnb/CnbApi.cs @@ -9,19 +9,23 @@ internal interface IBankApi { Task> GetTodayExchangeRates(); } - - internal sealed class BankApi : IBankApi + + /// + /// Api for Česká národní banka + /// + /// + internal sealed class CnbApi : IBankApi { private readonly HttpClient _httpClient; - public BankApi(HttpClient httpClient) + public CnbApi(HttpClient httpClient) { _httpClient = httpClient; } public async Task> GetTodayExchangeRates() { - var exchangeRatesEndpoint = "https://api.cnb.cz/cnbapi/exrates/daily?lang=EN"; + const string exchangeRatesEndpoint = "https://api.cnb.cz/cnbapi/exrates/daily?lang=EN"; var response = await _httpClient.GetFromJsonAsync(exchangeRatesEndpoint); return response.Rates; } diff --git a/jobs/Backend/Task/Cnb/CnbRates.cs b/jobs/Backend/Task/Cnb/CnbRates.cs index c4b786eed9..b9a38eb8a5 100644 --- a/jobs/Backend/Task/Cnb/CnbRates.cs +++ b/jobs/Backend/Task/Cnb/CnbRates.cs @@ -9,26 +9,17 @@ internal class CnbExchangeRates [JsonInclude] public IEnumerable Rates { get; private set; } } -/// -/// "validFor": "2019-05-17", - // "ordaaaaer": 94, - // "country": "Austrálie", - // "currency": "dolar", - // "amount": 1, - // "currencyCode": "AUD", - // "rate": 15.858aaa -/// + internal sealed class CnbExchangeRate { [JsonPropertyName("currencyCode")] - [JsonInclude] - public string CurrencyCode { get; private set; } + public string CurrencyCode { get; init; } - [JsonPropertyName("currencyCode")] - public int Amount { get; set; } + [JsonPropertyName("amount")] + public int Amount { get; init; } [JsonPropertyName("rate")] - public decimal Rate { get; set; } + public decimal Rate { get; init; } public decimal RealRate => Rate / Amount; } From ac59a17e228b751d889745cd27eca2498a608883 Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 18:40:14 +0100 Subject: [PATCH 05/14] feat: validate currency init --- jobs/Backend/Task/Currency.cs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/Currency.cs index 92265e25e4..8a4ff7da99 100644 --- a/jobs/Backend/Task/Currency.cs +++ b/jobs/Backend/Task/Currency.cs @@ -1,10 +1,29 @@ -namespace ExchangeRateUpdater +using System; +using System.Linq; + +namespace ExchangeRateUpdater { internal sealed class Currency { internal static readonly Currency Czk = new Currency("CZK"); + public Currency(string code) { + if (string.IsNullOrWhiteSpace(code)) + { + throw new ArgumentNullException(nameof(code), + "code is null or empty. Provide a valid ISO 4217 currency code."); + } + + if (code.Length != 3) + { + throw new ArgumentException("code must be exactly 3 characters.", nameof(code)); + } + + if (!code.All(x=>char.IsUpper(x) && char.IsLetter(x))) + { + throw new ArgumentException("code must be consist of all upper characters", nameof(code)); + } Code = code; } From f36f6c0f140ce96e612e1ac3c7cdb0c8534ad895 Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 18:40:36 +0100 Subject: [PATCH 06/14] feat: implement provider and task --- jobs/Backend/Task/ExchangeRateProvider.cs | 19 ++++++++++++++++--- jobs/Backend/Task/ExchangeRateUpdater.csproj | 10 ++++++++-- jobs/Backend/Task/ExchangeRateUpdater.sln | 6 ++++++ jobs/Backend/Task/Program.cs | 14 ++++++++++---- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs index 6f82a97fbe..caf79f7015 100644 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/ExchangeRateProvider.cs @@ -1,19 +1,32 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using ExchangeRateUpdater.Cnb; namespace ExchangeRateUpdater { - public class ExchangeRateProvider + internal sealed class ExchangeRateProvider { + private readonly IBankApi _bankApi; + + public ExchangeRateProvider(IBankApi bankApi) + { + _bankApi = bankApi; + } + /// /// 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 async Task> GetExchangeRates(IEnumerable currencies) { - return Enumerable.Empty(); + var cnbRates = await _bankApi.GetTodayExchangeRates(); + var selectedRates = cnbRates.Select(x => ExchangeRate.FromCZK(new Currency(x.CurrencyCode), x.RealRate)) + // TODO: seems should be filtered by source currency as well, but our source is always CZK I only need target currency then? + .Where(x => currencies.Contains(x.TargetCurrency)); + return selectedRates; } } } diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 2fc654a12b..4bf7daea6d 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -4,5 +4,11 @@ Exe net6.0 - - \ No newline at end of file + + + + <_Parameter1>ExchangeRateUpdater.Tests + + + + diff --git a/jobs/Backend/Task/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater.sln index 89be84daff..eeaa54c6c1 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.sln +++ b/jobs/Backend/Task/ExchangeRateUpdater.sln @@ -5,6 +5,8 @@ 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.Tests", "..\Task.Tests\ExchangeRateUpdater.Tests\ExchangeRateUpdater.Tests.csproj", "{FDD27E7D-8663-44F9-B247-610426632DA6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ 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 + {FDD27E7D-8663-44F9-B247-610426632DA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDD27E7D-8663-44F9-B247-610426632DA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDD27E7D-8663-44F9-B247-610426632DA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDD27E7D-8663-44F9-B247-610426632DA6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 379a69b1f8..ed62084df1 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using ExchangeRateUpdater.Cnb; namespace ExchangeRateUpdater { - public static class Program + internal static class Program { private static IEnumerable currencies = new[] { @@ -19,12 +23,14 @@ 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 httpClient = new HttpClient(); + var cnbApi = new CnbApi(httpClient); + var provider = new ExchangeRateProvider(cnbApi); + var rates = await provider.GetExchangeRates(currencies); Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); foreach (var rate in rates) From c7650c8a3b6d8e4478387c4ae5832ea450b6c186 Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 18:41:16 +0100 Subject: [PATCH 07/14] feat: add tests for CnbCurrency and Currency --- .../Cnb/CnbRatesTests.cs | 22 +++++++++ .../CurrencyTests.cs | 46 +++++++++++++++++++ .../ExchangeRateUpdater.Tests.csproj | 27 +++++++++++ 3 files changed, 95 insertions(+) create mode 100644 jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/CurrencyTests.cs create mode 100644 jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj diff --git a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs index e69de29bb2..a66e1160fb 100644 --- a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs +++ b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs @@ -0,0 +1,22 @@ +using ExchangeRateUpdater.Cnb; + +namespace ExchangeRateUpdater.Tests.Cnb +{ + public class CnbRatesTests + { + + [Theory] + [InlineData(1, 32.2, 32.2)] + [InlineData(100, 32.2, 0.322)] + [InlineData(50, 32.2, 0.644)] + public void ShouldCalculateRealAmount(int amount, decimal rate, decimal expectedAmount) + { + var cnbRate = new CnbExchangeRate() + { + Amount = amount, + Rate = rate + }; + Assert.Equal(expectedAmount, cnbRate.RealRate); + } + } +} diff --git a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/CurrencyTests.cs b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/CurrencyTests.cs new file mode 100644 index 0000000000..0b93ac28d1 --- /dev/null +++ b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/CurrencyTests.cs @@ -0,0 +1,46 @@ +namespace ExchangeRateUpdater.Tests; + +public class CurrencyTests +{ + [Fact] + public void ShouldInitNewCurrency() + { + var curr = new Currency("USD"); + Assert.NotNull(curr); + Assert.Equal("USD", curr.Code); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData(" ")] + [InlineData(" ")] + public void ShouldThrowArgumentExceptionWhenCodeIsNullOrWhitespace(string code) + { + Assert.Throws(() => new Currency(code)); + } + + [Theory] + [InlineData("USDC")] + [InlineData("US")] + public void ShouldThrowIfInvalidLength(string code) + { + Assert.Throws(() => new Currency(code)); + } + + [Theory] + [InlineData("usd")] + [InlineData("Usd")] + public void ShouldThrowIfCaseMismatch(string code) + { + Assert.Throws(() => new Currency(code)); + } + + [Theory] + [InlineData("AB1")] + [InlineData("AR%")] + public void ShouldThrowIfNotAllLetters(string code) + { + Assert.Throws(() => new Currency(code)); + } +} diff --git a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj new file mode 100644 index 0000000000..95c2debd68 --- /dev/null +++ b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + From 8c90160f8396caa68cc1612300672295f0debfa9 Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 18:55:43 +0100 Subject: [PATCH 08/14] feat: add test for ExchangeRateProvider override hashcode and equals for ExchangeRate --- .../ExchangeRateProviderTests.cs | 53 +++++++++++++++++++ .../ExchangeRateUpdater.Tests.csproj | 1 + jobs/Backend/Task/ExchangeRate.cs | 19 ++++++- jobs/Backend/Task/Program.cs | 1 + 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs diff --git a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs new file mode 100644 index 0000000000..2fe24322da --- /dev/null +++ b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs @@ -0,0 +1,53 @@ +using ExchangeRateUpdater.Cnb; +using Moq; + +namespace ExchangeRateUpdater.Tests +{ + public class ExchangeRateProviderTests + { + private readonly Mock _bankApiMock = new Mock(); + private readonly ExchangeRateProvider _exchangeRateProvider; + + public ExchangeRateProviderTests() + { + _exchangeRateProvider = new ExchangeRateProvider(_bankApiMock.Object); + } + + [Fact] + public async Task ShouldProvideOnlyCurrenciesSpecifiedInList() + { + _bankApiMock.Setup(x => x.GetTodayExchangeRates()).ReturnsAsync(new List() + { + new CnbExchangeRate() + { + Amount = 1, + Rate = 2.0M, + CurrencyCode = "USD" + }, + new CnbExchangeRate() + { + Amount = 1, + Rate = 1.0M, + CurrencyCode = "EUR" + }, + new CnbExchangeRate() + { + Amount = 1, + Rate = 3.0M, + CurrencyCode = "AUD" + }, + }); + + var currencies = new List { new Currency("USD"), new Currency("EUR") }; + + var result = await _exchangeRateProvider.GetExchangeRates(currencies); + + var expected = new List() + { + ExchangeRate.FromCZK(new Currency("USD"), 2.0M), + ExchangeRate.FromCZK(new Currency("EUR"), 1.0M), + }; + Assert.Equal(expected, result); + } + } +} diff --git a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj index 95c2debd68..201c86bb0d 100644 --- a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj +++ b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/jobs/Backend/Task/ExchangeRate.cs b/jobs/Backend/Task/ExchangeRate.cs index fbf3de61e4..35ceea330e 100644 --- a/jobs/Backend/Task/ExchangeRate.cs +++ b/jobs/Backend/Task/ExchangeRate.cs @@ -1,4 +1,6 @@ -namespace ExchangeRateUpdater +using System; + +namespace ExchangeRateUpdater { internal sealed class ExchangeRate { @@ -26,6 +28,21 @@ public static ExchangeRate FromCZK(Currency targetCurrency, decimal value) return new ExchangeRate(Currency.Czk, targetCurrency, value); } + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) return true; + if (obj is null) return false; + if (obj is not ExchangeRate other) return false; + + return Value == other.Value && SourceCurrency.Equals(other.SourceCurrency) && + TargetCurrency.Equals(other.TargetCurrency); + } + + public override int GetHashCode() + { + return HashCode.Combine(Value, SourceCurrency, TargetCurrency); + } + public override string ToString() { return $"{SourceCurrency}/{TargetCurrency}={Value}"; diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index ed62084df1..034cc82b9e 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using ExchangeRateUpdater.Cnb; +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] namespace ExchangeRateUpdater { internal static class Program From 0c414b23a4ed423433f7f9c3719890a74bcde069 Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 18:59:02 +0100 Subject: [PATCH 09/14] feat: add empty filter test --- .../ExchangeRateProviderTests.cs | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs index 2fe24322da..2b2d671e4d 100644 --- a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs +++ b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs @@ -15,6 +15,32 @@ public ExchangeRateProviderTests() [Fact] public async Task ShouldProvideOnlyCurrenciesSpecifiedInList() + { + ArrangeApiRates(); + + var currencies = new List { new Currency("USD"), new Currency("EUR") }; + var result = await _exchangeRateProvider.GetExchangeRates(currencies); + + var expected = new List() + { + ExchangeRate.FromCZK(new Currency("USD"), 2.0M), + ExchangeRate.FromCZK(new Currency("EUR"), 1.0M), + }; + Assert.Equal(expected, result); + } + + [Fact] + public async Task ShouldReturnEmptyListIfEmptyRequestedCurrencies() + { + ArrangeApiRates(); + + var currencies = Enumerable.Empty(); + + var result = await _exchangeRateProvider.GetExchangeRates(currencies); + Assert.Empty(result); + } + + private void ArrangeApiRates() { _bankApiMock.Setup(x => x.GetTodayExchangeRates()).ReturnsAsync(new List() { @@ -37,17 +63,6 @@ public async Task ShouldProvideOnlyCurrenciesSpecifiedInList() CurrencyCode = "AUD" }, }); - - var currencies = new List { new Currency("USD"), new Currency("EUR") }; - - var result = await _exchangeRateProvider.GetExchangeRates(currencies); - - var expected = new List() - { - ExchangeRate.FromCZK(new Currency("USD"), 2.0M), - ExchangeRate.FromCZK(new Currency("EUR"), 1.0M), - }; - Assert.Equal(expected, result); } } } From a5d5e9e2caf74ab04e36c3504d7d10a6315d3d6a Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 19:03:02 +0100 Subject: [PATCH 10/14] feat: enable nullable ref type, throw if currencies null --- .../ExchangeRateProviderTests.cs | 10 ++++++++++ jobs/Backend/Task/ExchangeRateProvider.cs | 9 +++++++-- jobs/Backend/Task/ExchangeRateUpdater.csproj | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs index 2b2d671e4d..93c6a00e6d 100644 --- a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs +++ b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs @@ -39,6 +39,16 @@ public async Task ShouldReturnEmptyListIfEmptyRequestedCurrencies() var result = await _exchangeRateProvider.GetExchangeRates(currencies); Assert.Empty(result); } + + [Fact] + public async Task ShouldThrowIfExpectedCurrenciesAreNull() + { + ArrangeApiRates(); + + IEnumerable currencies = null!; // whoopsie + var result = _exchangeRateProvider.GetExchangeRates(currencies); + await Assert.ThrowsAsync(()=> result); + } private void ArrangeApiRates() { diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs index caf79f7015..303a3bac4b 100644 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/ExchangeRateProvider.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ExchangeRateUpdater.Cnb; @@ -20,8 +21,12 @@ public ExchangeRateProvider(IBankApi bankApi) /// 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 async Task> GetExchangeRates(IEnumerable currencies) + public async Task> GetExchangeRates(IEnumerable? currencies) { + if (currencies == null) + { + throw new ArgumentNullException(nameof(currencies), "currencies is null"); + } var cnbRates = await _bankApi.GetTodayExchangeRates(); var selectedRates = cnbRates.Select(x => ExchangeRate.FromCZK(new Currency(x.CurrencyCode), x.RealRate)) // TODO: seems should be filtered by source currency as well, but our source is always CZK I only need target currency then? diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 4bf7daea6d..b889bb1fb8 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -3,6 +3,7 @@ Exe net6.0 + enable From 2388b5f6f5fca28628ad393086b9f1e41269e2fb Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 19:06:24 +0100 Subject: [PATCH 11/14] refactor: consider nullable ref warnings --- jobs/Backend/Task/Cnb/CnbApi.cs | 7 ++++++- jobs/Backend/Task/Cnb/CnbRates.cs | 13 +++++-------- jobs/Backend/Task/Currency.cs | 2 +- jobs/Backend/Task/ExchangeRate.cs | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/jobs/Backend/Task/Cnb/CnbApi.cs b/jobs/Backend/Task/Cnb/CnbApi.cs index 7a0b7317fd..f3ca2f4e27 100644 --- a/jobs/Backend/Task/Cnb/CnbApi.cs +++ b/jobs/Backend/Task/Cnb/CnbApi.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks; @@ -27,6 +28,10 @@ public async Task> GetTodayExchangeRates() { const string exchangeRatesEndpoint = "https://api.cnb.cz/cnbapi/exrates/daily?lang=EN"; var response = await _httpClient.GetFromJsonAsync(exchangeRatesEndpoint); + if (response == null) + { + throw new InvalidOperationException("Unable to get exchange rates. Response was null."); + } return response.Rates; } } diff --git a/jobs/Backend/Task/Cnb/CnbRates.cs b/jobs/Backend/Task/Cnb/CnbRates.cs index b9a38eb8a5..7daf45f516 100644 --- a/jobs/Backend/Task/Cnb/CnbRates.cs +++ b/jobs/Backend/Task/Cnb/CnbRates.cs @@ -7,19 +7,16 @@ internal class CnbExchangeRates { [JsonPropertyName("rates")] [JsonInclude] - public IEnumerable Rates { get; private set; } + public IEnumerable Rates { get; private set; } = null!; // required and json don't work well } - + internal sealed class CnbExchangeRate { - [JsonPropertyName("currencyCode")] - public string CurrencyCode { get; init; } + [JsonPropertyName("currencyCode")] public string CurrencyCode { get; init; } = null!; // required and json don't work well - [JsonPropertyName("amount")] - public int Amount { get; init; } + [JsonPropertyName("amount")] public int Amount { get; init; } - [JsonPropertyName("rate")] - public decimal Rate { get; init; } + [JsonPropertyName("rate")] public decimal Rate { get; init; } public decimal RealRate => Rate / Amount; } diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/Currency.cs index 8a4ff7da99..6f1287a9c9 100644 --- a/jobs/Backend/Task/Currency.cs +++ b/jobs/Backend/Task/Currency.cs @@ -32,7 +32,7 @@ public Currency(string code) /// public string Code { get; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) return true; if (obj is null) return false; diff --git a/jobs/Backend/Task/ExchangeRate.cs b/jobs/Backend/Task/ExchangeRate.cs index 35ceea330e..13972717fa 100644 --- a/jobs/Backend/Task/ExchangeRate.cs +++ b/jobs/Backend/Task/ExchangeRate.cs @@ -28,7 +28,7 @@ public static ExchangeRate FromCZK(Currency targetCurrency, decimal value) return new ExchangeRate(Currency.Czk, targetCurrency, value); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) return true; if (obj is null) return false; From f8367eeb6215136dd17b34aed24c870c32c25ef7 Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 19:06:58 +0100 Subject: [PATCH 12/14] chore: format --- jobs/Backend/Task/Cnb/CnbApi.cs | 2 +- jobs/Backend/Task/Currency.cs | 4 ++-- jobs/Backend/Task/Program.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jobs/Backend/Task/Cnb/CnbApi.cs b/jobs/Backend/Task/Cnb/CnbApi.cs index f3ca2f4e27..72381741e3 100644 --- a/jobs/Backend/Task/Cnb/CnbApi.cs +++ b/jobs/Backend/Task/Cnb/CnbApi.cs @@ -10,7 +10,7 @@ internal interface IBankApi { Task> GetTodayExchangeRates(); } - + /// /// Api for Česká národní banka /// diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/Currency.cs index 6f1287a9c9..ffaa3329b8 100644 --- a/jobs/Backend/Task/Currency.cs +++ b/jobs/Backend/Task/Currency.cs @@ -6,7 +6,7 @@ namespace ExchangeRateUpdater internal sealed class Currency { internal static readonly Currency Czk = new Currency("CZK"); - + public Currency(string code) { if (string.IsNullOrWhiteSpace(code)) @@ -20,7 +20,7 @@ public Currency(string code) throw new ArgumentException("code must be exactly 3 characters.", nameof(code)); } - if (!code.All(x=>char.IsUpper(x) && char.IsLetter(x))) + if (!code.All(x => char.IsUpper(x) && char.IsLetter(x))) { throw new ArgumentException("code must be consist of all upper characters", nameof(code)); } diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 034cc82b9e..93b09ab63f 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -9,7 +9,7 @@ [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] namespace ExchangeRateUpdater { - internal static class Program + internal static class Program { private static IEnumerable currencies = new[] { From ae7653cfd2f82c2ad19181644e76a163265827b2 Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 19:07:22 +0100 Subject: [PATCH 13/14] chore: format tests --- .../ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs | 2 +- .../Task.Tests/ExchangeRateUpdater.Tests/CurrencyTests.cs | 4 ++-- .../ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs index a66e1160fb..3a20d492f8 100644 --- a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs +++ b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/Cnb/CnbRatesTests.cs @@ -4,7 +4,7 @@ namespace ExchangeRateUpdater.Tests.Cnb { public class CnbRatesTests { - + [Theory] [InlineData(1, 32.2, 32.2)] [InlineData(100, 32.2, 0.322)] diff --git a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/CurrencyTests.cs b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/CurrencyTests.cs index 0b93ac28d1..8051cc4690 100644 --- a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/CurrencyTests.cs +++ b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/CurrencyTests.cs @@ -27,7 +27,7 @@ public void ShouldThrowIfInvalidLength(string code) { Assert.Throws(() => new Currency(code)); } - + [Theory] [InlineData("usd")] [InlineData("Usd")] @@ -35,7 +35,7 @@ public void ShouldThrowIfCaseMismatch(string code) { Assert.Throws(() => new Currency(code)); } - + [Theory] [InlineData("AB1")] [InlineData("AR%")] diff --git a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs index 93c6a00e6d..b35aacaa6f 100644 --- a/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs +++ b/jobs/Backend/Task.Tests/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs @@ -28,7 +28,7 @@ public async Task ShouldProvideOnlyCurrenciesSpecifiedInList() }; Assert.Equal(expected, result); } - + [Fact] public async Task ShouldReturnEmptyListIfEmptyRequestedCurrencies() { @@ -39,7 +39,7 @@ public async Task ShouldReturnEmptyListIfEmptyRequestedCurrencies() var result = await _exchangeRateProvider.GetExchangeRates(currencies); Assert.Empty(result); } - + [Fact] public async Task ShouldThrowIfExpectedCurrenciesAreNull() { @@ -47,7 +47,7 @@ public async Task ShouldThrowIfExpectedCurrenciesAreNull() IEnumerable currencies = null!; // whoopsie var result = _exchangeRateProvider.GetExchangeRates(currencies); - await Assert.ThrowsAsync(()=> result); + await Assert.ThrowsAsync(() => result); } private void ArrangeApiRates() From 556d64946ac2e45f5df97a308a43577a09adc4a3 Mon Sep 17 00:00:00 2001 From: erehonvl Date: Thu, 27 Nov 2025 19:09:59 +0100 Subject: [PATCH 14/14] feat: add Readme.md --- jobs/Backend/Task/Readme.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 jobs/Backend/Task/Readme.md diff --git a/jobs/Backend/Task/Readme.md b/jobs/Backend/Task/Readme.md new file mode 100644 index 0000000000..3f09688417 --- /dev/null +++ b/jobs/Backend/Task/Readme.md @@ -0,0 +1,16 @@ +# Hello! + + +## Prerequisites .net6, very high probability you already have it :) + +## Run +```bash +cd jobs/Backend/Task/ +dotnet run +``` + +## Run Test +```bash +cd jobs/Backend/Task/ +dotnet test +```