Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1061664
feat: move currencies to static class
sergiomadd Jan 19, 2026
901451f
feat: move entities to core and add custom exception
sergiomadd Jan 19, 2026
63644dc
feat: add config and needed packages
sergiomadd Jan 19, 2026
dae32ad
feat: add http client and policy
sergiomadd Jan 19, 2026
4d94bb7
feat: add exchange rate source and DTOs
sergiomadd Jan 19, 2026
f6785b7
feat: implement ExchangeRateProvider
sergiomadd Jan 19, 2026
cc485ea
feat: update program.cs to use new implementation
sergiomadd Jan 19, 2026
fd00d64
fix: properly throw exception when source deserialized data is null
sergiomadd Jan 20, 2026
fba7d9b
test: add test project
sergiomadd Jan 20, 2026
f391942
test: add unit test cases for CnbApiExchangeRateSource
sergiomadd Jan 20, 2026
64a5969
test: add unit test cases for ExchangeRateProvider
sergiomadd Jan 20, 2026
782f930
fix: skip exchange rate if same as base currency
sergiomadd Jan 20, 2026
93db532
fix: move exchangeRateDTO null check before loop
sergiomadd Jan 20, 2026
2b61e7f
chore: move projects inside own folders away from solution
sergiomadd Jan 20, 2026
5dde551
ci: add testing yml file
sergiomadd Jan 20, 2026
812e8ca
refactor: convert DTO classes to records
sergiomadd Jan 21, 2026
2457757
refactor: extract exchange rate processing logic
sergiomadd Jan 21, 2026
0d641ca
feat: add GetExchangeRatesFromDay to ExchangeRateProvider
sergiomadd Jan 21, 2026
d705298
feat: add specific date querying to CnbApiExchangeRateSource
sergiomadd Jan 21, 2026
4bc6744
feat: add DateValidator
sergiomadd Jan 21, 2026
92d8030
feat: add MenuHandler for a small console menu
sergiomadd Jan 21, 2026
1137571
feat: add menu function for GetDateExchangeRates
sergiomadd Jan 21, 2026
224ab0b
test: add unit test cases for GetExchangeRatesFromDay
sergiomadd Jan 21, 2026
ec39856
test: add unit test cases for DateValidator
sergiomadd Jan 21, 2026
23547de
feat: add CompareExchangeRatesBetweenDates feature to ExchangeRatePro…
sergiomadd Jan 21, 2026
182884e
feat: add menu function for GetComparedExchangeRates
sergiomadd Jan 21, 2026
9e54973
test: add unit test cases for CompareExchangeRatesBetweenDates
sergiomadd Jan 21, 2026
4adf088
fix: don't allow second date to be same as first date on compare
sergiomadd Jan 21, 2026
47b3181
feat: add ascii title to menu and ascii helper
sergiomadd Jan 21, 2026
3a4db1a
docs: add in code documentation
sergiomadd Jan 21, 2026
e60782e
fix: correctly invalidate when second date is same as first date in c…
sergiomadd Jan 21, 2026
8c36faa
fix: replace logger with console write on user relevant prints
sergiomadd Jan 22, 2026
d453fb9
feat: improve efficiency of ExchangeRateProvider methods
sergiomadd Jan 22, 2026
3b8c017
fix: set content root relative to app context
sergiomadd Jan 22, 2026
17ce147
fix: rework ascii helper to load using app context
sergiomadd Jan 22, 2026
6b331f0
docs: add README.md and preview image
sergiomadd Jan 22, 2026
4f94dd0
fix: typo on menu option 3 and update readme
sergiomadd Jan 22, 2026
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: .NET CI Build and Test

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build-and-test:
runs-on: ubuntu-latest

defaults:
run:
working-directory: jobs/Backend/Task

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 6.0.x

- name: Restore dependencies
run: dotnet restore ExchangeRateUpdater.sln

- name: Build solution
run: dotnet build ExchangeRateUpdater.sln --configuration Release --no-restore

- name: Run tests
run: dotnet test ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj --no-build --verbosity normal
19 changes: 0 additions & 19 deletions jobs/Backend/Task/ExchangeRateProvider.cs

This file was deleted.

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

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

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

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<None Update="appsettings.Test.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<Content Include="UnitTests\Data\**\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

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

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
using ExchangeRateUpdater.Application.DTOs;
using ExchangeRateUpdater.Core.Exceptions;
using ExchangeRateUpdater.Infrastructure.Options;
using ExchangeRateUpdater.Infrastructure.Sources;
using ExchangeRateUpdater.Tests.UnitTests.Data;
using ExchangeRateUpdater.Tests.UnitTests.HttpMessageHandlers;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Moq.Protected;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.Json;
using Xunit;

namespace ExchangeRateUpdater.Tests.UnitTests
{
public class CnbApiExchangeRateSourceTest
{
private readonly Mock<ILogger<CnbApiExchangeRateSource>> _loggerMock;
private readonly IConfiguration _configuration;

public CnbApiExchangeRateSourceTest()
{
_configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.Test.json", optional: false)
.Build();
_loggerMock = new Mock<ILogger<CnbApiExchangeRateSource>>();
}

[Fact]
public async Task GetDailyExchangeRates_ThrowsExchangeRateSourceException_WhenRequestTimesOut()
{
// Arrange
var httpClient = new HttpClient(new TimeoutHttpMessageHandler())
{
BaseAddress = new Uri(_configuration["CnbApi:BaseUrl"])
};
var options = Options.Create(new CnbApiOptions
{
DailyRatesPath = "exrates/daily?lang=EN"
});

var source = new CnbApiExchangeRateSource(_loggerMock.Object, options, httpClient);

// Act
var ex = await Assert.ThrowsAsync<ExchangeRateSourceException>(() => source.GetDailyExchangeRates());

// Assert
Assert.IsType<TaskCanceledException>(ex.InnerException);
}

[Fact]
public async Task GetDailyExchangeRates_ThrowsExchangeRateSourceException_WhenHttpStatusIsError()
{
//Arrange
var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
var httpClient = new HttpClient(new FakeHttpMessageHandler(response))
{
BaseAddress = new Uri("https://api.cnb.cz/cnbapi/")
};
var options = Options.Create(new CnbApiOptions
{
DailyRatesPath = "exrates/daily?lang=EN"
});

var source = new CnbApiExchangeRateSource(_loggerMock.Object, options, httpClient);

//Act
var ex = await Assert.ThrowsAsync<ExchangeRateSourceException>(() => source.GetDailyExchangeRates());

//Assert
Assert.IsType<HttpRequestException>(ex.InnerException);
}

[Fact]
public async Task GetDailyExchangeRates_ThrowsExchangeRateSourceException_WhenJsonIsInvalid()
{
//Arrange
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("this is not json")
};
var httpClient = new HttpClient(new FakeHttpMessageHandler(response))
{
BaseAddress = new Uri("https://api.cnb.cz/cnbapi/")
};
var options = Options.Create(new CnbApiOptions
{
DailyRatesPath = "exrates/daily?lang=EN"
});

var source = new CnbApiExchangeRateSource(_loggerMock.Object, options, httpClient);

//Act
var ex = await Assert.ThrowsAsync<ExchangeRateSourceException>(() => source.GetDailyExchangeRates());

//Assert
Assert.IsType<JsonException>(ex.InnerException);
}

[Fact]
public async Task GetDailyExchangeRates_ThrowsExchangeRateSourceException_WhenJsonIsNull()
{
// Arrange
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("null")
};
var httpClient = new HttpClient(new FakeHttpMessageHandler(response))
{
BaseAddress = new Uri("https://api.cnb.cz/cnbapi/")
};
var options = Options.Create(new CnbApiOptions
{
DailyRatesPath = "exrates/daily?lang=EN"
});

var source = new CnbApiExchangeRateSource(_loggerMock.Object, options, httpClient);

// Act
var ex = await Assert.ThrowsAsync<ExchangeRateSourceException>(() => source.GetDailyExchangeRates());

// Assert
Assert.IsType<JsonException>(ex.InnerException);
}

[Fact]
public async Task GetDailyExchangeRates_ThrowsExchangeRateSourceException_WhenRatesPropertyIsMissing()
{
// Arrange
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JsonTestDataLoader.Load("CnbApi/dailyRates_noRates.json"), Encoding.UTF8, "application/json")
};
var httpClient = new HttpClient(new FakeHttpMessageHandler(response))
{
BaseAddress = new Uri("https://api.cnb.cz/cnbapi/")
};
var options = Options.Create(new CnbApiOptions
{
DailyRatesPath = "exrates/daily?lang=EN"
});

var source = new CnbApiExchangeRateSource(_loggerMock.Object, options, httpClient);

// Act
var ex = await Assert.ThrowsAsync<ExchangeRateSourceException>(() => source.GetDailyExchangeRates());

// Assert
Assert.IsType<JsonException>(ex.InnerException);
}

[Fact]
public async Task GetDailyExchangeRates_ReturnsEmptyList_WhenRatesArrayIsEmpty()
{
// Arrange
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JsonTestDataLoader.Load("CnbApi/dailyRates_emptyRates.json"), Encoding.UTF8, "application/json")
};
var httpClient = new HttpClient(new FakeHttpMessageHandler(response))
{
BaseAddress = new Uri("https://api.cnb.cz/cnbapi/")
};
var options = Options.Create(new CnbApiOptions
{
DailyRatesPath = "exrates/daily?lang=EN"
});

var source = new CnbApiExchangeRateSource(_loggerMock.Object, options, httpClient);

// Act
var result = await source.GetDailyExchangeRates();

// Assert
Assert.Empty(result);
}

[Fact]
public async Task GetDailyExchangeRates_ReturnsRates_WhenResponseIsValid()
{
// Arrange
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JsonTestDataLoader.Load("CnbApi/dailyrates_valid.json"), Encoding.UTF8, "application/json")
};
var httpClient = new HttpClient(new FakeHttpMessageHandler(response))
{
BaseAddress = new Uri("https://api.cnb.cz/cnbapi/")
};
var options = Options.Create(new CnbApiOptions
{
DailyRatesPath = "exrates/daily?lang=EN"
});

var source = new CnbApiExchangeRateSource(
_loggerMock.Object,
options,
httpClient);

// Act
var result = await source.GetDailyExchangeRates();

// Assert
var rates = result.ToList();
Assert.Equal(3, rates.Count);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"rates": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"invalid property": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"rates":
[
{
"country": "Austrálie",
"currency": "dolar",
"amount": 1,
"code": "AUD",
"rate": 15.123
},
{
"country": "Eurozóna",
"currency": "euro",
"amount": 1,
"code": "EUR",
"rate": 24.567
},
{
"country": "Spojené státy americké",
"currency": "dolar",
"amount": 1,
"code": "USD",
"rate": 22.345
}
]
}
Loading