Skip to content
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
20 changes: 0 additions & 20 deletions jobs/Backend/Task/Currency.cs

This file was deleted.

23 changes: 0 additions & 23 deletions jobs/Backend/Task/ExchangeRate.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ExchangeRateUpdater.Core.ApiVendors;
using ExchangeRateUpdater.Core.Models;
using ExchangeRateUpdater.Core.Providers;
using FluentAssertions;
using Moq;
using Xunit;

namespace ExchangeRateUpdater.Core.Tests;

public class CzechNationalBankExchangeRateProviderTests
{
[Fact]
public async Task GetExchangeRates_EmptyInput_ReturnsEmpty_AndDoesNotCallVendor()
{
// Arrange
var vendorMock = new Mock<IExchangeRateVendor>(MockBehavior.Strict);
var sut = new CzechNationalBankExchangeRateProvider(vendorMock.Object);

// Act
var result = await sut.GetExchangeRates([]);

// Assert
result.Should().BeEmpty();
vendorMock.Verify(v => v.GetExchangeRates(It.IsAny<string>()), Times.Never);
}

[Fact]
public async Task GetExchangeRates_FiltersToRequestedCurrencies_AndCallsVendorOnce()
{
// Arrange
var vendorMock = new Mock<IExchangeRateVendor>();
vendorMock
.Setup(v => v.GetExchangeRates("CZK"))
.ReturnsAsync([
new(new Currency("CZK"), new Currency("USD"), 0.043m),
new(new Currency("CZK"), new Currency("EUR"), 0.039m),
new(new Currency("CZK"), new Currency("GBP"), 0.034m)
]);

var sut = new CzechNationalBankExchangeRateProvider(vendorMock.Object);

var requested = new[] { new Currency("USD"), new Currency("EUR") };

// Act
var result = await sut.GetExchangeRates(requested);

// Assert
result.Should().HaveCount(2);
result.Select(r => r.TargetCurrency.ToString()).Should().BeEquivalentTo(new[] { "USD", "EUR" });
result.All(r => r.SourceCurrency.ToString() == "CZK").Should().BeTrue();
vendorMock.Verify(v => v.GetExchangeRates("CZK"), Times.Once);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ExchangeRateUpdater.Core\ExchangeRateUpdater.Core.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Linq;
using ExchangeRateUpdater.Core.Configuration;
using ExchangeRateUpdater.Core.Configuration.Options;
using ExchangeRateUpdater.Core.Providers;
using ExchangeRateUpdater.Core.Models;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;

namespace ExchangeRateUpdater.Core.Tests;

public class ServiceConfigurationTests
{
[Fact]
public void AddCore_BindsCurrencies_AndRegistersProvider()
{
// Arrange
var inMemory = new[]
{
new KeyValuePair<string, string?>("Currencies:0", "usd"),
new KeyValuePair<string, string?>("Currencies:1", " EUR ")
};
var configuration = new ConfigurationBuilder().AddInMemoryCollection(inMemory).Build();
var services = new ServiceCollection();

services.AddSingleton(Moq.Mock.Of<ApiVendors.IExchangeRateVendor>());

// Act
services.AddCore(configuration);
var sp = services.BuildServiceProvider();

// Assert
sp.GetService<IExchangeRateProvider>().Should().NotBeNull();

var opts = sp.GetRequiredService<IOptions<CurrencyOptions>>().Value;
opts.Currencies.Should().HaveCount(2);
opts.Currencies.Select(c => c.ToString()).Should().BeEquivalentTo(new[] { "usd", " EUR " }.Select(s => new Currency(s).ToString()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using ExchangeRateUpdater.Core.Models;

namespace ExchangeRateUpdater.Core.ApiVendors;

public interface IExchangeRateVendor
{
Task<List<ExchangeRate>> GetExchangeRates(string baseCurrencyCode);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace ExchangeRateUpdater.Core.AppExceptions;

public class VendorFailureException(string message) : Exception(message);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using ExchangeRateUpdater.Core.Models;

namespace ExchangeRateUpdater.Core.Configuration.Options;

public class CurrencyOptions
{
public Currency[] Currencies { get; set; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using ExchangeRateUpdater.Core.Configuration.Options;
using ExchangeRateUpdater.Core.Models;
using ExchangeRateUpdater.Core.Providers;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ExchangeRateUpdater.Core.Configuration;

public static class ServiceConfiguration
{
public static IServiceCollection AddCore(this IServiceCollection services, IConfiguration configuration)
{
services.AddMemoryCache();
services.AddTransient<IExchangeRateProvider, CzechNationalBankExchangeRateProvider>();
services.AddOptions<CurrencyOptions>().Configure(opts =>
{
var currencies = configuration.GetSection("Currencies").Get<string[]>() ?? [];
opts.Currencies = currencies.Select(s => new Currency(s)).ToArray();
})
.ValidateOnStart();

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions jobs/Backend/Task/ExchangeRateUpdater.Core/Models/Currency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace ExchangeRateUpdater.Core.Models;

public class Currency(
string code
)
{
/// <summary>
/// Three-letter ISO 4217 code of the currency.
/// </summary>
private string Code { get; } = code;

public override string ToString()
{
return Code;
}
}
19 changes: 19 additions & 0 deletions jobs/Backend/Task/ExchangeRateUpdater.Core/Models/ExchangeRate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace ExchangeRateUpdater.Core.Models;

public class ExchangeRate(
Currency sourceCurrency,
Currency targetCurrency,
decimal value
)
{
public Currency SourceCurrency { get; } = sourceCurrency;

public Currency TargetCurrency { get; } = targetCurrency;

public decimal Value { get; } = value;

public override string ToString()
{
return $"{SourceCurrency}/{TargetCurrency}={Value}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using ExchangeRateUpdater.Core.ApiVendors;
using ExchangeRateUpdater.Core.Models;
using Microsoft.Extensions.Caching.Memory;

namespace ExchangeRateUpdater.Core.Providers;

public class CzechNationalBankExchangeRateProvider(
IExchangeRateVendor exchangeRateVendor,
IMemoryCache? cache = null
) : IExchangeRateProvider
{
private const string BaseCurrencyCode = "CZK";
private const string CacheKey = $"ExchangeRates:{BaseCurrencyCode}";
private readonly IMemoryCache _cache = cache ?? new MemoryCache(new MemoryCacheOptions());

public async Task<List<ExchangeRate>> GetExchangeRates(IEnumerable<Currency> currencies)
{
currencies = currencies.ToArray();
if (!currencies.Any()) return [];

var allRates = await _cache.GetOrCreateAsync(CacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
return await exchangeRateVendor.GetExchangeRates(BaseCurrencyCode);
}) ?? [];

return allRates
.Where(rate => currencies.Any(currency => currency.ToString() == rate.TargetCurrency.ToString()))
.ToList();
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using ExchangeRateUpdater.Core.Models;

namespace ExchangeRateUpdater
{
public class ExchangeRateProvider
namespace ExchangeRateUpdater.Core.Providers;
public class ExchangeRateProvider : IExchangeRateProvider
{
/// <summary>
/// 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.
/// </summary>
public IEnumerable<ExchangeRate> GetExchangeRates(IEnumerable<Currency> currencies)
public Task<List<ExchangeRate>> GetExchangeRates(IEnumerable<Currency> currencies)
{
return Enumerable.Empty<ExchangeRate>();
return Task.FromResult(new List<ExchangeRate>());
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using ExchangeRateUpdater.Core.Models;

namespace ExchangeRateUpdater.Core.Providers;

public interface IExchangeRateProvider
{
Task<List<ExchangeRate>> GetExchangeRates(IEnumerable<Currency> currencies);
}
Loading