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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@
node_modules
bower_components
npm-debug.log

.vs/
132 changes: 132 additions & 0 deletions jobs/Backend/Task.Test/CnbExchangeRateClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
namespace ExchangeReaderUpdater.Test
{
using ExchangeRateUpdater.Exceptions;
using ExchangeRateUpdater.ExchangeClients;
using ExchangeRateUpdater.Settings;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Moq.Protected;
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

public class CnbExchangeRateClientTests
{
[Fact]
public async Task getDailyRatesAsync_returnsContent_whenResponseIsSuccess()
{
var handlerMock = new Mock<HttpMessageHandler>();
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("daily content")
};

handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response)
.Verifiable();

using var httpClient = new HttpClient(handlerMock.Object);

var optionsMock = new Mock<IOptions<ExchangeRateProviderSettings>>();
optionsMock.SetupGet(o => o.Value).Returns(new ExchangeRateProviderSettings { CnbUrl = "http://test" });

var client = new CnbExchangeRateClient(httpClient, optionsMock.Object, NullLogger<CnbExchangeRateClient>.Instance);

var result = await client.GetDailyRatesAsync(CancellationToken.None);

result.Should().Be("daily content");

handlerMock.Protected().Verify(
"SendAsync",
Times.Once(),
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
ItExpr.IsAny<CancellationToken>());
}

[Fact]
public async Task getDailyRatesAsync_throwsExchangeRateUpdateException_onNonSuccessStatus()
{
var handlerMock = new Mock<HttpMessageHandler>();
var response = new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent("error")
};

handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response)
.Verifiable();

using var httpClient = new HttpClient(handlerMock.Object);

var optionsMock = new Mock<IOptions<ExchangeRateProviderSettings>>();
optionsMock.SetupGet(o => o.Value).Returns(new ExchangeRateProviderSettings { CnbUrl = "http://test" });

var client = new CnbExchangeRateClient(httpClient, optionsMock.Object, NullLogger<CnbExchangeRateClient>.Instance);

Func<Task> act = async () => await client.GetDailyRatesAsync(CancellationToken.None);

await act.Should().ThrowAsync<ExchangeRateUpdateException>()
.Where(ex => ex.InnerException is HttpRequestException);
}

[Fact]
public async Task getDailyRatesAsync_wrapsHandlerHttpRequestException()
{
var handlerMock = new Mock<HttpMessageHandler>();

handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ThrowsAsync(new HttpRequestException("network error"))
.Verifiable();

using var httpClient = new HttpClient(handlerMock.Object);

var optionsMock = new Mock<IOptions<ExchangeRateProviderSettings>>();
optionsMock.SetupGet(o => o.Value).Returns(new ExchangeRateProviderSettings { CnbUrl = "http://test" });

var client = new CnbExchangeRateClient(httpClient, optionsMock.Object, NullLogger<CnbExchangeRateClient>.Instance);

Func<Task> act = async () => await client.GetDailyRatesAsync(CancellationToken.None);

await act.Should().ThrowAsync<ExchangeRateUpdateException>()
.Where(ex => ex.InnerException is HttpRequestException && ex.InnerException.Message.Contains("network error"));
}

[Fact]
public async Task getDailyRatesAsync_propagatesCancellation()
{
var handlerMock = new Mock<HttpMessageHandler>();

handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ThrowsAsync(new TaskCanceledException())
.Verifiable();

using var httpClient = new HttpClient(handlerMock.Object);

var optionsMock = new Mock<IOptions<ExchangeRateProviderSettings>>();
optionsMock.SetupGet(o => o.Value).Returns(new ExchangeRateProviderSettings { CnbUrl = "http://test" });

var client = new CnbExchangeRateClient(httpClient, optionsMock.Object, NullLogger<CnbExchangeRateClient>.Instance);

Func<Task> act = async () => await client.GetDailyRatesAsync(new CancellationToken(true));

await act.Should().ThrowAsync<TaskCanceledException>();
}
}
}
168 changes: 168 additions & 0 deletions jobs/Backend/Task.Test/CnbExchangeRateDataParserTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
namespace ExchangeReaderUpdater.Test
{
using ExchangeRateUpdater.Models;
using ExchangeRateUpdater.Parsers;
using FluentAssertions;
using System.Linq;
using Xunit;

public class CnbExchangeRateDataParserTests
{
private readonly CnbExchangeRateDataParser _parser = new CnbExchangeRateDataParser();

[Fact]
public void parse_valid_single_line_returns_exchange_rate()
{
var currencies = new[] { new Currency("USD") };
var data = "25 Feb 2025 #11\nCountry|Currency|Amount|Code|Rate\nUSA|Dollar|1|USD|21.456\n";

var result = _parser.Parse(data, currencies).ToList();

result.Should().HaveCount(1);
result.Should().SatisfyRespectively(
first =>
{
first.SourceCurrency.Code.Should().Be("USD");
first.TargetCurrency.Code.Should().Be("CZK");
first.Value.Should().Be(21.456m);
});
}

[Fact]
public void parse_multiple_lines_returns_exchange_rates()
{
var currencies = new[] { new Currency("USD"), new Currency("EUR"), new Currency("AUD") };
var data = "25 Feb 2025 #11\nCountry|Currency|Amount|Code|Rate\nUSA|Dollar|1|USD|21.456\nEMU|euro|1|EUR|24.285\nAustralia|dollar|1|AUD|14.004";
var result = _parser.Parse(data, currencies).ToList();

result.Should().HaveCount(3);
result.Should().SatisfyRespectively(
first =>
{
first.SourceCurrency.Code.Should().Be("USD");
first.TargetCurrency.Code.Should().Be("CZK");
first.Value.Should().Be(21.456m);
},
second =>
{
second.SourceCurrency.Code.Should().Be("EUR");
second.TargetCurrency.Code.Should().Be("CZK");
second.Value.Should().Be(24.285m);
},
third =>
{
third.SourceCurrency.Code.Should().Be("AUD");
third.TargetCurrency.Code.Should().Be("CZK");
third.Value.Should().Be(14.004m);
});
}

[Fact]
public void parse_multiple_lines_returns_filtered_exchange_rates()
{
var currencies = new[] { new Currency("EUR"), new Currency("AUD") };
var data = "25 Feb 2025 #11\nCountry|Currency|Amount|Code|Rate\nUSA|Dollar|1|USD|21.456\nEMU|euro|1|EUR|24.285\nAustralia|dollar|1|AUD|14.004";
var result = _parser.Parse(data, currencies).ToList();

result.Should().HaveCount(2);
result.Should().SatisfyRespectively(
first =>
{
first.SourceCurrency.Code.Should().Be("EUR");
first.TargetCurrency.Code.Should().Be("CZK");
first.Value.Should().Be(24.285m);
},
second =>
{
second.SourceCurrency.Code.Should().Be("AUD");
second.TargetCurrency.Code.Should().Be("CZK");
second.Value.Should().Be(14.004m);
});
}

[Fact]
public void parse_invalid_column_count_skips_line()
{
var currencies = new[] { new Currency("USD") };
var data = "25 Feb 2025 #11\nCountry|Currency|Amount|Code|Rate\nUSA|Dollar|1|USD\n";

var result = _parser.Parse(data, currencies).ToList();

result.Should().HaveCount(0);
}

[Fact]
public void parse_whitespace_trims_code()
{
var currencies = new[] { new Currency("USD"), new Currency("CZK") };
var data = "25 Feb 2025 #11\nCountry|Currency|Amount|Code|Rate\n USA |Dollar|1| USD |21.456\n";

var result = _parser.Parse(data, currencies).ToList();

result.Should().HaveCount(1);
result.Should().SatisfyRespectively(
first =>
{
first.SourceCurrency.Code.Should().Be("USD");
first.TargetCurrency.Code.Should().Be("CZK");
first.Value.Should().Be(21.456m);
});
}

[Fact]
public void zero_rate_skips_line()
{
var currencies = new[] { new Currency("USD"), new Currency("CZK") };
var data = "25 Feb 2025 #11\nCountry|Currency|Amount|Code|Rate\nUSA|Dollar|1|USD|0\n";

var result = _parser.Parse(data, currencies).ToList();

result.Should().HaveCount(0);
}

[Fact]
public void non_numeric_rate_skips_line()
{
var currencies = new[] { new Currency("USD"), new Currency("CZK") };
var data = "25 Feb 2025 #11\nCountry|Currency|Amount|Code|Rate\nUSA|Dollar|1|USD|not_a_number\n";

var result = _parser.Parse(data, currencies).ToList();

result.Should().HaveCount(0);
}

[Fact]
public void parse_various_newline_formats()
{
var currencies = new[] { new Currency("USD"), new Currency("EUR") };
var data = "25 Feb 2025 #11\r\nCountry|Currency|Amount|Code|Rate\r\nUSA|Dollar|1|USD|21.456\nEMU|euro|1|EUR|24.285\rAustralia|dollar|1|AUD|14.004";

var result = _parser.Parse(data, currencies).ToList();

result.Should().HaveCount(2);
result.Select(r => r.SourceCurrency.Code).Should().Contain(new[] { "USD", "EUR" });
}

[Fact]
public void amount_zero_skips_line()
{
var currencies = new[] { new Currency("USD"), new Currency("CZK") };
var data = "25 Feb 2025 #11\nCountry|Currency|Amount|Code|Rate\nUSA|Dollar|0|USD|21.456\n";

var result = _parser.Parse(data, currencies).ToList();

result.Should().BeEmpty();
}

[Fact]
public void negative_rate_skips_line()
{
var currencies = new[] { new Currency("USD"), new Currency("CZK") };
var data = "25 Feb 2025 #11\nCountry|Currency|Amount|Code|Rate\nUSA|Dollar|1|USD|-21.456\n";

var result = _parser.Parse(data, currencies).ToList();

result.Should().BeEmpty();
}
}
}
88 changes: 88 additions & 0 deletions jobs/Backend/Task.Test/ExchangeRateProviderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
namespace ExchangeReaderUpdater.Test
{
using ExchangeRateUpdater;
using ExchangeRateUpdater.Exceptions;
using ExchangeRateUpdater.ExchangeClients;
using ExchangeRateUpdater.Models;
using ExchangeRateUpdater.Parsers;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

public class ExchangeRateProviderTests
{
[Fact]
public async Task get_exchangeRatesAsync_returnsParsedRates_fromClientData()
{
var mockClient = new Mock<IExchangeRateClient>();
var parser = new CnbExchangeRateDataParser();

var currencies = new[] { new Currency("USD"), new Currency("CZK") };
var rawData = "25 Feb 2025 #11\nCountry|Currency|Amount|Code|Rate\nUSA|Dollar|1|USD|21.456\n";

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

mockClient.Setup(c => c.GetDailyRatesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(rawData);

var provider = new ExchangeRateProvider(mockClient.Object, parser, NullLogger<ExchangeRateProvider>.Instance);

var result = (await provider.GetExchangeRatesAsync(currencies)).ToList();

result.Should().BeEquivalentTo(expectedRates);
mockClient.Verify(c => c.GetDailyRatesAsync(It.IsAny<CancellationToken>()), Times.Once);
}

[Fact]
public async Task getExchangeRatesAsync_nullCurrencies_throwsArgumentNullException()
{
var mockClient = new Mock<IExchangeRateClient>();
var mockParser = new Mock<IExchangeRateDataParser>();
var provider = new ExchangeRateProvider(mockClient.Object, mockParser.Object, NullLogger<ExchangeRateProvider>.Instance);

var act = async () => await provider.GetExchangeRatesAsync(null);

await act.Should().ThrowAsync<ArgumentNullException>();
}

[Fact]
public async Task getExchangeRatesAsync_emptyCurrencyList_returnsEmpty()
{
var mockClient = new Mock<IExchangeRateClient>();
var mockParser = new Mock<IExchangeRateDataParser>();
var provider = new ExchangeRateProvider(mockClient.Object, mockParser.Object, NullLogger<ExchangeRateProvider>.Instance);

var result = await provider.GetExchangeRatesAsync(Array.Empty<Currency>());

result.Should().BeEmpty();
mockClient.Verify(c => c.GetDailyRatesAsync(It.IsAny<CancellationToken>()), Times.Never);
mockParser.Verify(p => p.Parse(It.IsAny<string>(), It.IsAny<IEnumerable<Currency>>()), Times.Never);
}

[Fact]
public async Task getExchangeRatesAsync_propagatesExchangeRateUpdateExceptionFromClient()
{
var mockClient = new Mock<IExchangeRateClient>();
var mockParser = new Mock<IExchangeRateDataParser>();
var provider = new ExchangeRateProvider(mockClient.Object, mockParser.Object, NullLogger<ExchangeRateProvider>.Instance);

mockClient.Setup(c => c.GetDailyRatesAsync(It.IsAny<CancellationToken>()))
.ThrowsAsync(new ExchangeRateUpdateException("remote error"));

var currencies = new[] { new Currency("USD"), new Currency("CZK") };

var act = async () => await provider.GetExchangeRatesAsync(currencies);

await act.Should().ThrowAsync<ExchangeRateUpdateException>();
}
}
}
Loading