Skip to content

Implementation of ExchangeRateProvider as per Task spec #729

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using ExchangeRateUpdater;
using ExchangeRateUpdater.Models;
using ExchangeRateUpdater.Services;
using Moq;
using Xunit;

namespace ExchangeRateUpdaterTests.ExchangeRateProviderTests
{

public class ExchangeRateProviderTests
{
[Fact]
public async Task GetExchangeRatesAsync_ReturnsRatesFromService_WhenCurrenciesProvided()
{
// Arrange
var currencies = new[]
{
new Currency("CZK"),
new Currency("USD")
};

var expectedRates = new List<ExchangeRate>
{
new ExchangeRate(new Currency("CZK"), new Currency("USD"), 22.5m)
};

var serviceMock = new Mock<IExchangeRateProviderService>();
serviceMock.Setup(s => s.GetExchangeRateAsync(It.Is<IEnumerable<Currency>>(c => c.SequenceEqual(currencies))))
.ReturnsAsync(expectedRates);

var provider = new ExchangeRateProvider(serviceMock.Object);

// Act
var result = await provider.GetExchangeRatesAsync(currencies);

// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("USD", result.First().TargetCurrency.Code);
serviceMock.Verify(s => s.GetExchangeRateAsync(It.IsAny<IEnumerable<Currency>>()), Times.Once);
}

[Fact]
public async Task GetExchangeRatesAsync_ReturnsEmpty_WhenCurrencyListIsNull()
{
// Arrange
var serviceMock = new Mock<IExchangeRateProviderService>();
var provider = new ExchangeRateProvider(serviceMock.Object);

// Act
var result = await provider.GetExchangeRatesAsync(null);

// Assert
Assert.Empty(result);
serviceMock.Verify(s => s.GetExchangeRateAsync(It.IsAny<IEnumerable<Currency>>()), Times.Never);
}

[Fact]
public async Task GetExchangeRatesAsync_ReturnsEmpty_WhenCurrencyListIsEmpty()
{
// Arrange
var serviceMock = new Mock<IExchangeRateProviderService>();
var provider = new ExchangeRateProvider(serviceMock.Object);

// Act
var result = await provider.GetExchangeRatesAsync(new List<Currency>());

// Assert
Assert.Empty(result);
serviceMock.Verify(s => s.GetExchangeRateAsync(It.IsAny<IEnumerable<Currency>>()), Times.Never);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using ExchangeRateUpdater.Configuration;
using ExchangeRateUpdater.Models;
using ExchangeRateUpdater.Services;
using ExchangeRateUpdaterTests.StartupTests;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RichardSzalay.MockHttp;
using Xunit;

namespace ExchangeRateUpdater.Tests.Integration
{
public class ExchangeRateProviderIntegrationTests
{
[Fact]
public async Task GetExchangeRatesAsync_ReturnsCombinedRates_FromAllFetchers()
{
// Arrange

var mockHttp = new MockHttpMessageHandler();

mockHttp.When("https://mock-daily")
.Respond("text/plain", @"
11 Jun 2025 #123
Country|Currency|Amount|Code|Rate
United States|Dollar|1|USD|22,123");

mockHttp.When("https://mock-other")
.Respond("text/plain", @"
11 Jun 2025 #123
Country|Currency|Amount|Code|Rate|Source
Switzerland|Franc|1|CHF|25,456|SNB");

var httpClient = mockHttp.ToHttpClient();

var inMemorySettings = new Dictionary<string, string>
{
["CzechBankSettings:DailyRatesUrl"] = "https://mock-daily",
["CzechBankSettings:OtherCurrencyRatesUrl"] = "https://mock-other",
["CzechBankSettings:TimeoutSeconds"] = "10",
["CzechBankSettings:RetryCount"] = "2"
};

var config = new ConfigurationBuilder()
.AddInMemoryCollection(inMemorySettings)
.Build();

var services = new ServiceCollection();
var startup = new StartupForTest(config);
startup.ConfigureServices(services);

services.AddSingleton(httpClient);

var provider = services.BuildServiceProvider();

var httpFactory = new TestHttpClientFactory(httpClient);
services.AddSingleton<IHttpClientFactory>(httpFactory);

var finalProvider = services.BuildServiceProvider();

var exchangeRateProvider = finalProvider.GetRequiredService<IExchangeRateProviderService>();

// Act
var currencies = new[] { new Currency("USD"), new Currency("CHF") };
var rates = await exchangeRateProvider.GetExchangeRateAsync(currencies);

// Assert
Assert.NotEmpty(rates);
Assert.Contains(rates, r => r.TargetCurrency.Code == "USD");
Assert.Contains(rates, r => r.TargetCurrency.Code == "CHF");
}

// IHttpClientFactory override
private class TestHttpClientFactory : IHttpClientFactory
{
private readonly HttpClient _client;

public TestHttpClientFactory(HttpClient client)
{
_client = client;
}

public HttpClient CreateClient(string name) => _client;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.4" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2" />
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Task\ExchangeRateUpdater.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using ExchangeRateUpdater.Configuration;
using ExchangeRateUpdater.HttpClients;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using RichardSzalay.MockHttp;
using Xunit;

namespace ExchangeRateUpdaterTests.HttpClients
{
public class ExchangeRateFetcherTests
{
[Fact]
public async Task FetchAsync_ReturnsExpectedRawText()
{
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("https://mock-daily")
.Respond("text/plain", "Mock CNB Data");
var mockLogger = new Mock<ILogger<DailyExchangeRateFetcher>>();

var httpClient = mockHttp.ToHttpClient();

var fetcher = new DailyExchangeRateFetcher(
httpClient,
Options.Create(new CzechBankSettings { DailyRatesUrl = "https://mock-daily" }),
mockLogger.Object
);

var result = await fetcher.FetchAsync();
Assert.Equal("Mock CNB Data", result);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using ExchangeRateUpdater.Models;
using ExchangeRateUpdater.Parsers;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;

namespace ExchangeRateUpdater.Tests.Parsers
{
public class CzechNationalBankTextRateParserTests
{
private readonly CzechNationalBankTextRateParser _parser;

public CzechNationalBankTextRateParserTests()
{
var mockLogger = new Mock<ILogger<CzechNationalBankTextRateParser>>();
_parser = new CzechNationalBankTextRateParser(5, mockLogger.Object);
}

[Fact]
public void Parse_ParsesValidLine_Correctly()
{
var raw = @"
11 Jun 2025 #123
Country|Currency|Amount|Code|Rate
United States|Dollar|1|USD|22,123
";

var filter = new List<Currency> { new Currency("USD") };
var result = _parser.Parse(raw, filter, new Currency("CZK")).ToList();

Assert.Single(result);
var rate = result[0];
Assert.Equal("CZK", rate.SourceCurrency.Code);
Assert.Equal("USD", rate.TargetCurrency.Code);
Assert.Equal(22.123m, rate.Value);
}

[Fact]
public void Parse_SkipsLines_WithTooFewColumns()
{
var raw = @"
11 Jun 2025 #123
Country|Currency|Amount|Code|Rate
Broken|Line|Only|3
United States|Dollar|1|USD|22,123
";

var filter = new List<Currency> { new Currency("USD") };
var result = _parser.Parse(raw, filter, new Currency("CZK")).ToList();

Assert.Single(result);
Assert.Equal("USD", result[0].TargetCurrency.Code);
}

[Fact]
public void Parse_SkipsCurrencies_NotInFilter()
{
var raw = @"
11 Jun 2025 #123
Country|Currency|Amount|Code|Rate
United States|Dollar|1|USD|22,123
Eurozone|Euro|1|EUR|25,456
";

var filter = new List<Currency> { new Currency("EUR") };
var result = _parser.Parse(raw, filter, new Currency("CZK")).ToList();

Assert.Single(result);
Assert.Equal("EUR", result[0].TargetCurrency.Code);
}

[Fact]
public void Parse_NormalizesRate_BasedOnAmount()
{
var raw = @"
11 Jun 2025 #123
Country|Currency|Amount|Code|Rate
Japan|Yen|100|JPY|50,000
";

var filter = new List<Currency> { new Currency("JPY") };
var result = _parser.Parse(raw, filter, new Currency("CZK")).ToList();

Assert.Single(result);
Assert.Equal("JPY", result[0].TargetCurrency.Code);
Assert.Equal(0.500, (double)result[0].Value); // 50,000 / 100
}

[Fact]
public void Parse_ReturnsEmpty_WhenNoLinesMatch()
{
var raw = @"
11 Jun 2025 #123
Country|Currency|Amount|Code|Rate
United States|Dollar|1|USD|22,123
";

var filter = new List<Currency> { new Currency("EUR") }; // no EUR present
var result = _parser.Parse(raw, filter, new Currency("CZK")).ToList();

Assert.Empty(result);
}
}
}
Loading