Skip to content

Implement Exchange Rate Provider #474

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 1 commit 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
415 changes: 395 additions & 20 deletions .gitignore

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="FluentValidation" Version="11.7.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

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

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using ExchangeRateUpdater.Domain.Constants;
using ExchangeRateUpdater.Domain.Models.Response;
using ExchangeRateUpdater.Infrastructure.Connectors;

using Moq;
using Moq.Protected;

using System.Net;

namespace ExchangeRateUpdater.UnitTests.Infrastructure.Connectors
{
public class BaseCzechNationalBankConnectorTests
{
protected Mock<IHttpClientFactory> mockHttpClientFactory;
protected readonly HttpClient httpClient;
protected readonly Mock<HttpMessageHandler> mockHttpMessageHandler;

public BaseCzechNationalBankConnectorTests()
{
mockHttpMessageHandler = new Mock<HttpMessageHandler>();
httpClient = new HttpClient(mockHttpMessageHandler.Object)
{
BaseAddress = new Uri("https://api.cnb.cz/cnbapi/")
};
mockHttpClientFactory = new Mock<IHttpClientFactory>();
mockHttpClientFactory.Setup(clientFactory => clientFactory.CreateClient(BankConstants.CzechNationalBank.HttpClientIdentifier)).Returns(httpClient);
}

public CzechNationalBankConnector GetConnector()
{
return new CzechNationalBankConnector(mockHttpClientFactory.Object);
}

public void MockExchangeResponse(ExchangeRatesResponse? exchangeRatesResponse, HttpStatusCode statusCode)
{
var response = new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(
Newtonsoft.Json.JsonConvert.SerializeObject(exchangeRatesResponse),
Encoding.UTF8,
"application/json")
};

MockHttpMessageHandler(response);
}

private void MockHttpMessageHandler(HttpResponseMessage response)
{
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using ExchangeRateUpdater.Domain.Models;
using ExchangeRateUpdater.Domain.Models.Response;

using FluentAssertions;

using System.Net;

namespace ExchangeRateUpdater.UnitTests.Infrastructure.Connectors
{
public class CzechNationalBankConnectorTests : BaseCzechNationalBankConnectorTests
{
[Fact]
public async Task GetExchangeRates_NullExchangeRateResponse_ReturnsEmptyExchangeRates()
{
// Arrange
var expectedCurrencies = new List<Currency>
{
new Currency("USD"),
new Currency("EUR"),
new Currency("GBP")
};

ExchangeRatesResponse? exchangeRatesResponse = null;
MockExchangeResponse(exchangeRatesResponse, HttpStatusCode.OK);

var czechNationalBankConnector = GetConnector();

// Act
var exchangeRates = await czechNationalBankConnector.GetExchangeRates(expectedCurrencies);

// Assert
exchangeRates.Should().BeEmpty();
exchangeRates.Should().HaveCount(0);
}

[Fact]
public async Task GetExchangeRates_NewExchangeRateResponse_ReturnsEmptyExchangeRates()
{
// Arrange
var expectedCurrencies = new List<Currency>
{
new Currency("USD"),
new Currency("EUR"),
new Currency("GBP")
};

var exchangeRatesResponse = new ExchangeRatesResponse();
MockExchangeResponse(exchangeRatesResponse, HttpStatusCode.OK);

var czechNationalBankConnector = GetConnector();

// Act
var exchangeRates = await czechNationalBankConnector.GetExchangeRates(expectedCurrencies);

// Assert
exchangeRates.Should().BeEmpty();
exchangeRates.Should().HaveCount(0);
}

[Fact]
public async Task GetExchangeRates_NewExchangeRateResponseList_ReturnsEmptyExchangeRates()
{
// Arrange
var expectedCurrencies = new List<Currency>
{
new Currency("USD"),
new Currency("EUR"),
new Currency("GBP")
};

var exchangeRatesResponse = new ExchangeRatesResponse { Rates = new List<ExchangeRateResponse>() };
MockExchangeResponse(exchangeRatesResponse, HttpStatusCode.OK);

var czechNationalBankConnector = GetConnector();

// Act
var exchangeRates = await czechNationalBankConnector.GetExchangeRates(expectedCurrencies);

// Assert
exchangeRates.Should().BeEmpty();
exchangeRates.Should().HaveCount(0);
}

[Fact]
public async Task GetExchangeRates_ValidCurrencies_ReturnsExpectedExchangeRates()
{
// Arrange
var expectedCurrencies = new List<Currency>
{
new Currency("USD"),
new Currency("EUR"),
new Currency("GBP")
};

var exchangeRatesResponse = new ExchangeRatesResponse
{
Rates = new List<ExchangeRateResponse>
{
new ExchangeRateResponse { CurrencyCode = "USD", Rate = 23.128m, Amount = 1, Country = "USA" },
new ExchangeRateResponse { CurrencyCode = "EUR", Rate = 24.560m, Amount = 1, Country = "EMU" },
new ExchangeRateResponse { CurrencyCode = "GBP", Rate = 28.462m, Amount = 1, Country = "Velká Británie" }
}
};

MockExchangeResponse(exchangeRatesResponse, HttpStatusCode.OK);
var czechNationalBankConnector = GetConnector();

// Act
var exchangeRates = await czechNationalBankConnector.GetExchangeRates(expectedCurrencies);

// Assert
exchangeRates.Should().NotBeNullOrEmpty();
exchangeRates.Should().HaveCount(expectedCurrencies.Count);
}

[Fact]
public async Task GetExchangeRates_OnlyOneValidCurrencies_ReturnsOneExchangeRate()
{
// Arrange
var expectedCurrencies = new List<Currency>
{
new Currency("USD"),
};

var exchangeRatesResponse = new ExchangeRatesResponse
{
Rates = new List<ExchangeRateResponse>
{
new ExchangeRateResponse { CurrencyCode = "USD", Rate = 23.128m, Amount = 1, Country = "USA" },
new ExchangeRateResponse { CurrencyCode = "EUR", Rate = 24.560m, Amount = 1, Country = "EMU" },
new ExchangeRateResponse { CurrencyCode = "GBP", Rate = 28.462m, Amount = 1, Country = "Velká Británie" }
}
};

MockExchangeResponse(exchangeRatesResponse, HttpStatusCode.OK);
var czechNationalBankConnector = GetConnector();

// Act
var exchangeRates = await czechNationalBankConnector.GetExchangeRates(expectedCurrencies);

// Assert
exchangeRates.Should().NotBeNullOrEmpty();
exchangeRates.Should().HaveCount(1);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using ExchangeRateUpdater.Application.Banks;
using ExchangeRateUpdater.Domain.Enums;
using ExchangeRateUpdater.Infrastructure.Factories;

using FluentAssertions;

using Moq;

namespace ExchangeRateUpdater.UnitTests.Infrastructure.Factories
{
public class BankFactoryTests
{
[Fact]
public void Create_ValidBankIdentifier_ReturnsBankConnector()
{
// Arrange
var bankId = BankIdentifier.CzechNationalBank;
var mockBankConnector = new Mock<IBankConnector>();
mockBankConnector.Setup(bc => bc.BankIdentifier).Returns(bankId);

var mockBankConnectors = new List<IBankConnector> { mockBankConnector.Object };
var bankFactory = new BankFactory(mockBankConnectors);

// Act
var result = bankFactory.Create(bankId);

// Assert
result.Should().Be(mockBankConnector.Object);
}

[Fact]
public void Create_InvalidBankIdentifier_ThrowsException()
{
// Arrange
var bankId = 999999;
var mockBankConnectors = new List<IBankConnector>();
var bankFactory = new BankFactory(mockBankConnectors);

// Act
Action act = () => bankFactory.Create((BankIdentifier)bankId);

// Assert
act.Should().Throw<NotSupportedException>().WithMessage($"Bank '{bankId}' is not supported.");
}
}
}
6 changes: 6 additions & 0 deletions jobs/Backend/ExchangeRateUpdater.UnitTests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
global using System;
global using System.Collections.Generic;
global using System.Text;
global using System.Threading.Tasks;

global using Xunit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using ExchangeRateUpdater.Domain.Enums;
using ExchangeRateUpdater.Domain.Models;

using System.Collections.Generic;
using System.Threading.Tasks;

namespace ExchangeRateUpdater.Application.Banks
{
public interface IBankConnector
{
BankIdentifier BankIdentifier { get; }
Task<IEnumerable<ExchangeRate>> GetExchangeRates(IEnumerable<Currency> currencies);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ExchangeRateUpdater.Domain.Enums;

namespace ExchangeRateUpdater.Application.Banks
{
public interface IBankFactory
{
IBankConnector Create(BankIdentifier bankId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using ExchangeRateUpdater.Domain.Models;

using System.Collections.Generic;
using System.Threading.Tasks;

namespace ExchangeRateUpdater.Application.ExchangeProvider
{
public interface IExchangeRateProviderService
{
Task<IEnumerable<ExchangeRate>> GetExchangeRates(IEnumerable<Currency> currencies);
}
}
17 changes: 17 additions & 0 deletions jobs/Backend/ExchangeRateUpdater/Domain/Constants/BankConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace ExchangeRateUpdater.Domain.Constants
{
public class BankConstants
{
public class CzechNationalBank
{
public const string HttpClientIdentifier = "czech-exchange-rate-client";
public const string DefaultCurrency = "CZK";
}

public class DeNederlandscheBank
{
public const string HttpClientIdentifier = "dutch-exchange-rate-client";
public const string DefaultCurrency = "EUR";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace ExchangeRateUpdater.Domain.Enums
{
public enum BankIdentifier
{
CzechNationalBank = 1,
DeNederlandscheBank
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace ExchangeRateUpdater
namespace ExchangeRateUpdater.Domain.Models
{
public class Currency
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace ExchangeRateUpdater
namespace ExchangeRateUpdater.Domain.Models
{
public class ExchangeRate
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace ExchangeRateUpdater.Domain.Models.Response
{
public class ExchangeRatesResponse
{
[JsonPropertyName("rates")]
public List<ExchangeRateResponse> Rates { get; set; }
}

public class ExchangeRateResponse
{
[JsonPropertyName("validFor")]
public string ValidFor { get; set; }
[JsonPropertyName("order")]
public int Order { get; set; }
[JsonPropertyName("country")]
public string Country { get; set; }
[JsonPropertyName("currency")]
public string Currency { get; set; }
[JsonPropertyName("amount")]
public int Amount { get; set; }
[JsonPropertyName("currencyCode")]
public string CurrencyCode { get; set; }
[JsonPropertyName("rate")]
public decimal Rate { get; set; }
}
}
Loading