From 521c9e7e72046a64584190c6868f8a79562ffce5 Mon Sep 17 00:00:00 2001 From: Temirkhan Amanzhanov Date: Tue, 17 Mar 2026 15:11:16 +0100 Subject: [PATCH 1/2] Code cleanup --- .../GetAffirmationLanguagesQueryTests.cs | 7 ++-- .../Queries/GetAffirmationQueryTests.cs | 33 +++++++++---------- .../Extensions/NSubstituteExtensions.cs | 8 +++++ .../Application/DiConfig.cs | 5 +-- .../Queries/GetAffirmationQuery.cs | 6 ++-- .../Services/Language/ILanguageCodeMapper.cs | 8 ----- .../Application/Services/Mapping/IMapper.cs | 8 +++++ .../Language/AffirmationLanguageMapper.cs} | 10 +++--- .../DeepL/DeepLTranslatorClient.cs | 11 ++++--- .../DeepL/IDeepLTranslatorClient.cs | 2 +- AffirmationGenerator.Server/Program.cs | 3 +- 11 files changed, 55 insertions(+), 46 deletions(-) delete mode 100644 AffirmationGenerator.Server/Application/Services/Language/ILanguageCodeMapper.cs create mode 100644 AffirmationGenerator.Server/Application/Services/Mapping/IMapper.cs rename AffirmationGenerator.Server/Application/Services/{Language/AffirmationLanguageCodeMapper.cs => Mapping/Language/AffirmationLanguageMapper.cs} (52%) diff --git a/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationLanguagesQueryTests.cs b/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationLanguagesQueryTests.cs index 0d4018a..64e8ef3 100644 --- a/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationLanguagesQueryTests.cs +++ b/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationLanguagesQueryTests.cs @@ -1,4 +1,5 @@ using AffirmationGenerator.Server.Application.Queries; +using AffirmationGenerator.Server.Domain; using AffirmationGenerator.Server.Tests.Extensions; using NUnit.Framework; using Shouldly; @@ -23,9 +24,9 @@ public void Handle_ShouldReturnSortedLanguages() var result = _query.Handle(); // Assert - var languages = result.ShouldBeSuccess().Languages; + var response = result.ShouldBeSuccess(); - languages.Count.ShouldBeGreaterThan(0); - languages.ShouldBeInOrder(SortDirection.Ascending); + response.Languages.Count.ShouldBe(Enum.GetValues().Length); + response.Languages.ShouldBeInOrder(SortDirection.Ascending); } } diff --git a/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationQueryTests.cs b/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationQueryTests.cs index 7c028e3..3c13f05 100644 --- a/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationQueryTests.cs +++ b/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationQueryTests.cs @@ -1,8 +1,7 @@ using AffirmationGenerator.Server.Application.Models; using AffirmationGenerator.Server.Application.Queries; using AffirmationGenerator.Server.Application.Services.Affirmation; -using AffirmationGenerator.Server.Application.Services.Language; -using AffirmationGenerator.Server.Core; +using AffirmationGenerator.Server.Application.Services.Mapping; using AffirmationGenerator.Server.Domain; using AffirmationGenerator.Server.Infrastructure.DeepL; using AffirmationGenerator.Server.Tests.Extensions; @@ -17,17 +16,19 @@ namespace AffirmationGenerator.Server.Tests.Application.Queries; public sealed class GetAffirmationQueryTests : TestBase { private IDeepLTranslatorClient _translatorClient = null!; - private ILanguageCodeMapper _languageCodeMapper = null!; + private IMapper _affirmationLanguageMapper = null!; private IAffirmationService _affirmationService = null!; private GetAffirmationQuery _query = null!; + private const int MaxRequestsPerDay = 10; + [SetUp] public void SetUp() { _translatorClient = Substitute.For(); - _languageCodeMapper = Substitute.For>(); + _affirmationLanguageMapper = Substitute.For>(); _affirmationService = Substitute.For(); - _query = new GetAffirmationQuery(_translatorClient, _affirmationService, _languageCodeMapper); + _query = new GetAffirmationQuery(_translatorClient, _affirmationService, _affirmationLanguageMapper); } [Test] @@ -35,13 +36,12 @@ public async Task Handle_WhenLanguageIsEnglish_ShouldReturnUntranslatedAffirmati { // Arrange const string affirmationText = "Good day!"; - const int maxRequestsPerDay = 10; - _affirmationService.GetAffirmation().Returns(affirmationText); + _affirmationService.GetAffirmation().ReturnsSuccess(affirmationText); var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.English }; - _languageCodeMapper.Map(getAffirmationRequest.TargetLanguage).Returns(LanguageCode.English); + _affirmationLanguageMapper.Map(getAffirmationRequest.TargetLanguage).ReturnsSuccess(LanguageCode.English); // Act var result = await _query.Handle(getAffirmationRequest); @@ -51,10 +51,10 @@ public async Task Handle_WhenLanguageIsEnglish_ShouldReturnUntranslatedAffirmati response.TargetLanguage.ShouldBe(AffirmationLanguage.English); response.Text.ShouldBe(affirmationText); - response.RemainingCount.ShouldBeLessThan(maxRequestsPerDay); + response.RemainingCount.ShouldBeLessThan(MaxRequestsPerDay); await _affirmationService.Received(1).GetAffirmation(); - _languageCodeMapper.Received(1).Map(AffirmationLanguage.English); + _affirmationLanguageMapper.Received(1).Map(AffirmationLanguage.English); await _translatorClient.DidNotReceiveWithAnyArgs().Translate(Arg.Any(), Arg.Any(), Arg.Any()); } @@ -64,13 +64,12 @@ public async Task Handle_WhenLanguageIsGerman_ShouldReturnTranslatedAffirmationI // Arrange const string affirmationText = "Good day!"; const string affirmationTextInGerman = "Guten Tag!"; - const int maxRequestsPerDay = 10; - _affirmationService.GetAffirmation().Returns(affirmationText); + _affirmationService.GetAffirmation().ReturnsSuccess(affirmationText); var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.German }; - _languageCodeMapper.Map(getAffirmationRequest.TargetLanguage).Returns(LanguageCode.German); + _affirmationLanguageMapper.Map(getAffirmationRequest.TargetLanguage).ReturnsSuccess(LanguageCode.German); _translatorClient.Translate(affirmationText, LanguageCode.English, LanguageCode.German).Returns(affirmationTextInGerman); // Act @@ -81,10 +80,10 @@ public async Task Handle_WhenLanguageIsGerman_ShouldReturnTranslatedAffirmationI response.TargetLanguage.ShouldBe(AffirmationLanguage.German); response.Text.ShouldBe(affirmationTextInGerman); - response.RemainingCount.ShouldBeLessThan(maxRequestsPerDay); + response.RemainingCount.ShouldBeLessThan(MaxRequestsPerDay); await _affirmationService.Received(1).GetAffirmation(); - _languageCodeMapper.Received(1).Map(AffirmationLanguage.German); + _affirmationLanguageMapper.Received(1).Map(AffirmationLanguage.German); await _translatorClient.Received(1).Translate(affirmationText, LanguageCode.English, LanguageCode.German); } @@ -92,7 +91,7 @@ public async Task Handle_WhenLanguageIsGerman_ShouldReturnTranslatedAffirmationI public async Task Handle_WhenNoAffirmation_ShouldReturnError() { // Arrange - _affirmationService.GetAffirmation().Returns(Result.Error(new AffirmationNotFound())); + _affirmationService.GetAffirmation().ReturnsError(); var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.German }; @@ -103,7 +102,7 @@ public async Task Handle_WhenNoAffirmation_ShouldReturnError() result.ShouldBeError().ShouldBeOfType(); await _affirmationService.Received(1).GetAffirmation(); - _languageCodeMapper.DidNotReceiveWithAnyArgs().Map(Arg.Any()); + _affirmationLanguageMapper.DidNotReceiveWithAnyArgs().Map(Arg.Any()); await _translatorClient.DidNotReceiveWithAnyArgs().Translate(Arg.Any(), Arg.Any(), Arg.Any()); } } diff --git a/AffirmationGenerator.Server.Tests/Extensions/NSubstituteExtensions.cs b/AffirmationGenerator.Server.Tests/Extensions/NSubstituteExtensions.cs index 80bc2f0..84be44b 100644 --- a/AffirmationGenerator.Server.Tests/Extensions/NSubstituteExtensions.cs +++ b/AffirmationGenerator.Server.Tests/Extensions/NSubstituteExtensions.cs @@ -13,4 +13,12 @@ public static class NSubstituteExtensions public ConfiguredCall ReturnsError() where TError : ErrorDetails, new() => value.Returns(Result.Error(new TError())); } + + extension(Result value) + { + public ConfiguredCall ReturnsSuccess(T returnThis) => value.Returns(Result.Success(returnThis)); + + public ConfiguredCall ReturnsError() + where TError : ErrorDetails, new() => value.Returns(Result.Error(new TError())); + } } diff --git a/AffirmationGenerator.Server/Application/DiConfig.cs b/AffirmationGenerator.Server/Application/DiConfig.cs index dc42906..45e9bd4 100644 --- a/AffirmationGenerator.Server/Application/DiConfig.cs +++ b/AffirmationGenerator.Server/Application/DiConfig.cs @@ -1,6 +1,7 @@ using AffirmationGenerator.Server.Application.Queries; using AffirmationGenerator.Server.Application.Services.Affirmation; -using AffirmationGenerator.Server.Application.Services.Language; +using AffirmationGenerator.Server.Application.Services.Mapping; +using AffirmationGenerator.Server.Application.Services.Mapping.Language; using AffirmationGenerator.Server.Domain; namespace AffirmationGenerator.Server.Application; @@ -21,7 +22,7 @@ public IServiceCollection AddApplication(IConfiguration configuration) services.AddScoped(); services.AddScoped(); - services.AddScoped, AffirmationLanguageCodeMapper>(); + services.AddScoped, AffirmationLanguageMapper>(); return services; } diff --git a/AffirmationGenerator.Server/Application/Queries/GetAffirmationQuery.cs b/AffirmationGenerator.Server/Application/Queries/GetAffirmationQuery.cs index 7b36461..9d3e39b 100644 --- a/AffirmationGenerator.Server/Application/Queries/GetAffirmationQuery.cs +++ b/AffirmationGenerator.Server/Application/Queries/GetAffirmationQuery.cs @@ -1,6 +1,6 @@ using AffirmationGenerator.Server.Application.Models; using AffirmationGenerator.Server.Application.Services.Affirmation; -using AffirmationGenerator.Server.Application.Services.Language; +using AffirmationGenerator.Server.Application.Services.Mapping; using AffirmationGenerator.Server.Core; using AffirmationGenerator.Server.Core.Extensions; using AffirmationGenerator.Server.Domain; @@ -12,13 +12,13 @@ namespace AffirmationGenerator.Server.Application.Queries; public sealed class GetAffirmationQuery( IDeepLTranslatorClient translatorClient, IAffirmationService affirmationService, - ILanguageCodeMapper languageCodeMapper + IMapper affirmationLanguageMapper ) { public async Task> Handle(GetAffirmationRequest request) => await ( from affirmation in affirmationService.GetAffirmation() - from targetLanguageCode in languageCodeMapper.Map(request.TargetLanguage) + from targetLanguageCode in affirmationLanguageMapper.Map(request.TargetLanguage) from translatedAffirmation in Translate(affirmation, targetLanguageCode) select ToResponse(request.TargetLanguage, translatedAffirmation) ); diff --git a/AffirmationGenerator.Server/Application/Services/Language/ILanguageCodeMapper.cs b/AffirmationGenerator.Server/Application/Services/Language/ILanguageCodeMapper.cs deleted file mode 100644 index 38ce087..0000000 --- a/AffirmationGenerator.Server/Application/Services/Language/ILanguageCodeMapper.cs +++ /dev/null @@ -1,8 +0,0 @@ -using AffirmationGenerator.Server.Core; - -namespace AffirmationGenerator.Server.Application.Services.Language; - -public interface ILanguageCodeMapper -{ - Result Map(T language); -} diff --git a/AffirmationGenerator.Server/Application/Services/Mapping/IMapper.cs b/AffirmationGenerator.Server/Application/Services/Mapping/IMapper.cs new file mode 100644 index 0000000..7ed3ef7 --- /dev/null +++ b/AffirmationGenerator.Server/Application/Services/Mapping/IMapper.cs @@ -0,0 +1,8 @@ +using AffirmationGenerator.Server.Core; + +namespace AffirmationGenerator.Server.Application.Services.Mapping; + +public interface IMapper +{ + Result Map(TFrom value); +} diff --git a/AffirmationGenerator.Server/Application/Services/Language/AffirmationLanguageCodeMapper.cs b/AffirmationGenerator.Server/Application/Services/Mapping/Language/AffirmationLanguageMapper.cs similarity index 52% rename from AffirmationGenerator.Server/Application/Services/Language/AffirmationLanguageCodeMapper.cs rename to AffirmationGenerator.Server/Application/Services/Mapping/Language/AffirmationLanguageMapper.cs index 158cec9..b62c6a4 100644 --- a/AffirmationGenerator.Server/Application/Services/Language/AffirmationLanguageCodeMapper.cs +++ b/AffirmationGenerator.Server/Application/Services/Mapping/Language/AffirmationLanguageMapper.cs @@ -2,17 +2,17 @@ using AffirmationGenerator.Server.Domain; using DeepL; -namespace AffirmationGenerator.Server.Application.Services.Language; +namespace AffirmationGenerator.Server.Application.Services.Mapping.Language; -public sealed class AffirmationLanguageCodeMapper : ILanguageCodeMapper +public sealed class AffirmationLanguageMapper : IMapper { - public Result Map(AffirmationLanguage language) => - language switch + public Result Map(AffirmationLanguage value) => + value switch { AffirmationLanguage.English => LanguageCode.English, AffirmationLanguage.German => LanguageCode.German, AffirmationLanguage.Czech => LanguageCode.Czech, AffirmationLanguage.French => LanguageCode.French, - _ => Result.Error(new InvalidLanguageCode(language.ToString())), + _ => Result.Error(new InvalidLanguageCode(value.ToString())), }; } diff --git a/AffirmationGenerator.Server/Infrastructure/DeepL/DeepLTranslatorClient.cs b/AffirmationGenerator.Server/Infrastructure/DeepL/DeepLTranslatorClient.cs index b926975..a747034 100644 --- a/AffirmationGenerator.Server/Infrastructure/DeepL/DeepLTranslatorClient.cs +++ b/AffirmationGenerator.Server/Infrastructure/DeepL/DeepLTranslatorClient.cs @@ -8,11 +8,11 @@ public sealed class DeepLTranslatorClient(IOptions { private DeepLTranslatorClientOptions Options => options.Value; - public async Task Translate(string text, string sourceLanguage, string targetLanguage) + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode) { - if (string.IsNullOrWhiteSpace(targetLanguage)) + if (string.IsNullOrWhiteSpace(sourceLanguageCode) || string.IsNullOrWhiteSpace(targetLanguageCode)) { - logger.LogError("Unable to translate text. Target language is not set"); + logger.LogError("Unable to translate text. Source or target language is not set"); return string.Empty; } @@ -23,9 +23,10 @@ public async Task Translate(string text, string sourceLanguage, string t try { - var textResult = await client.TranslateTextAsync(text, sourceLanguage, targetLanguage); + var textResult = await client.TranslateTextAsync(text, sourceLanguageCode, targetLanguageCode); - logger.LogInformation("Billed characters {BilledCharacters}", textResult.BilledCharacters); + if (logger.IsEnabled(LogLevel.Information)) + logger.LogInformation("Billed characters {BilledCharacters}", textResult.BilledCharacters); return textResult.Text; } diff --git a/AffirmationGenerator.Server/Infrastructure/DeepL/IDeepLTranslatorClient.cs b/AffirmationGenerator.Server/Infrastructure/DeepL/IDeepLTranslatorClient.cs index 9215919..2b933e2 100644 --- a/AffirmationGenerator.Server/Infrastructure/DeepL/IDeepLTranslatorClient.cs +++ b/AffirmationGenerator.Server/Infrastructure/DeepL/IDeepLTranslatorClient.cs @@ -2,5 +2,5 @@ namespace AffirmationGenerator.Server.Infrastructure.DeepL; public interface IDeepLTranslatorClient { - Task Translate(string text, string sourceLanguage, string targetLanguage); + Task Translate(string text, string sourceLanguageCode, string targetLanguageCode); } diff --git a/AffirmationGenerator.Server/Program.cs b/AffirmationGenerator.Server/Program.cs index c795853..8c8dfb4 100644 --- a/AffirmationGenerator.Server/Program.cs +++ b/AffirmationGenerator.Server/Program.cs @@ -4,8 +4,7 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services.AddApi().AddApplication(builder.Configuration).AddInfrastructure(builder.Configuration); -builder.Services.AddHealthChecks(); +builder.Services.AddApi().AddApplication(builder.Configuration).AddInfrastructure(builder.Configuration).AddHealthChecks(); // Configure the HTTP request pipeline. var app = builder.Build(); From 48d53d99122c7c625ae959527ae04abe8d2b35a8 Mon Sep 17 00:00:00 2001 From: Temirkhan Amanzhanov Date: Tue, 17 Mar 2026 16:29:36 +0100 Subject: [PATCH 2/2] Real time timer for affirmation reset --- AffirmationGenerator.Client/src/App.tsx | 22 ++++-- .../Affirmation/RemainingItemsText.tsx | 78 ++++++++++++++++--- .../models/remainingAffirmationsResponse.ts | 1 + .../Queries/GetAffirmationQueryTests.cs | 12 +-- .../GetRemainingAffirmationsQueryTests.cs | 5 +- .../Models/RemainingAffirmationsResponse.cs | 2 + .../Queries/GetAffirmationQuery.cs | 4 +- .../Queries/GetRemainingAffirmationsQuery.cs | 9 ++- .../Affirmation/AffirmationService.cs | 16 ++-- .../Affirmation/IAffirmationService.cs | 6 +- .../Infrastructure/Redis/IRedisClient.cs | 2 + .../Infrastructure/Redis/RedisClient.cs | 2 + 12 files changed, 122 insertions(+), 37 deletions(-) diff --git a/AffirmationGenerator.Client/src/App.tsx b/AffirmationGenerator.Client/src/App.tsx index be27199..87452cd 100644 --- a/AffirmationGenerator.Client/src/App.tsx +++ b/AffirmationGenerator.Client/src/App.tsx @@ -31,15 +31,16 @@ function App() { .then((languages) => Object.entries(languages).map(([code, label]) => ({ code, label }))), }); - const { data: remainingCount } = useQuery({ + const { data: remainingData } = useQuery({ queryKey: ["remainingAffirmations"], queryFn: () => axios .get("/affirmations/remaining") - .then((response) => response.data.remainingCount), + .then((response) => response.data), }); - const remainingAffirmations = remainingCount ?? 0; + const remainingAffirmations = remainingData?.remainingCount ?? 0; + const resetInSeconds = remainingData?.resetInSeconds ?? 0; const maxAffirmationsMessage = "Achieved maximum amount of affirmations per day. Come back tomorrow for more affirmations! 😁"; @@ -64,11 +65,18 @@ function App() { axios .get("/affirmations", { params: { targetLanguage: targetLanguage } }) .then((response) => response.data) - .then((data) => { + .then(async (data) => { setAffirmationText(data.text); setDisplayedText(""); - queryClient.setQueryData(["remainingAffirmations"], data.remainingCount); - if (data.remainingCount === 0) setErrorMessage(maxAffirmationsMessage); + if (data.remainingCount === 0) { + await queryClient.invalidateQueries({ queryKey: ["remainingAffirmations"] }); + setErrorMessage(maxAffirmationsMessage); + } else { + queryClient.setQueryData(["remainingAffirmations"], { + remainingCount: data.remainingCount, + resetInSeconds: 0, + } as RemainingAffirmationsResponse); + } setIsShaking(true); setTimeout(() => setIsShaking(false), 500); }) @@ -90,7 +98,7 @@ function App() {
}> - + = 4) return "text-green-600"; - if (count >= 2) return "text-yellow-500"; +function formatTime(totalSeconds: number) { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; +} + +function getCountColor(count: number) { + if (count >= 4) return "text-green-600"; + if (count >= 2) return "text-yellow-500"; + + return "text-red-600"; +} + +function RemainingItemsText({ count, resetInSeconds }: RemainingAffirmationsTextProps) { + const [secondsLeft, setSecondsLeft] = useState(null); + const intervalRef = useRef | null>(null); + + useEffect(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + + if (count > 0 || resetInSeconds == 0) return; + + const targetTime = Date.now() + Math.max(0, Math.ceil(resetInSeconds)) * 1000; + + const updateTimer = () => { + const remaining = Math.max(0, Math.ceil((targetTime - Date.now()) / 1000)); + + setSecondsLeft(remaining); + + if (remaining <= 0 && intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + + updateTimer(); + + intervalRef.current = setInterval(updateTimer, 1000); - return "text-red-600"; - } + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [count, resetInSeconds]); return (
- Remaining affirmations:{" "} - - {count} - + {count === 0 && secondsLeft != null && secondsLeft > 0 ? ( + <> + Resets in:{" "} + + {formatTime(secondsLeft)} + + + ) : ( + <> + Remaining affirmations:{" "} + + {count} + + + )}
); } diff --git a/AffirmationGenerator.Client/src/models/remainingAffirmationsResponse.ts b/AffirmationGenerator.Client/src/models/remainingAffirmationsResponse.ts index eb3ebc0..4b47c0c 100644 --- a/AffirmationGenerator.Client/src/models/remainingAffirmationsResponse.ts +++ b/AffirmationGenerator.Client/src/models/remainingAffirmationsResponse.ts @@ -1,5 +1,6 @@ type RemainingAffirmationsResponse = { remainingCount: number; + resetInSeconds: number; }; export default RemainingAffirmationsResponse; diff --git a/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationQueryTests.cs b/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationQueryTests.cs index 3c13f05..a6ac0a5 100644 --- a/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationQueryTests.cs +++ b/AffirmationGenerator.Server.Tests/Application/Queries/GetAffirmationQueryTests.cs @@ -37,7 +37,7 @@ public async Task Handle_WhenLanguageIsEnglish_ShouldReturnUntranslatedAffirmati // Arrange const string affirmationText = "Good day!"; - _affirmationService.GetAffirmation().ReturnsSuccess(affirmationText); + _affirmationService.Get().ReturnsSuccess(affirmationText); var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.English }; @@ -53,7 +53,7 @@ public async Task Handle_WhenLanguageIsEnglish_ShouldReturnUntranslatedAffirmati response.Text.ShouldBe(affirmationText); response.RemainingCount.ShouldBeLessThan(MaxRequestsPerDay); - await _affirmationService.Received(1).GetAffirmation(); + await _affirmationService.Received(1).Get(); _affirmationLanguageMapper.Received(1).Map(AffirmationLanguage.English); await _translatorClient.DidNotReceiveWithAnyArgs().Translate(Arg.Any(), Arg.Any(), Arg.Any()); } @@ -65,7 +65,7 @@ public async Task Handle_WhenLanguageIsGerman_ShouldReturnTranslatedAffirmationI const string affirmationText = "Good day!"; const string affirmationTextInGerman = "Guten Tag!"; - _affirmationService.GetAffirmation().ReturnsSuccess(affirmationText); + _affirmationService.Get().ReturnsSuccess(affirmationText); var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.German }; @@ -82,7 +82,7 @@ public async Task Handle_WhenLanguageIsGerman_ShouldReturnTranslatedAffirmationI response.Text.ShouldBe(affirmationTextInGerman); response.RemainingCount.ShouldBeLessThan(MaxRequestsPerDay); - await _affirmationService.Received(1).GetAffirmation(); + await _affirmationService.Received(1).Get(); _affirmationLanguageMapper.Received(1).Map(AffirmationLanguage.German); await _translatorClient.Received(1).Translate(affirmationText, LanguageCode.English, LanguageCode.German); } @@ -91,7 +91,7 @@ public async Task Handle_WhenLanguageIsGerman_ShouldReturnTranslatedAffirmationI public async Task Handle_WhenNoAffirmation_ShouldReturnError() { // Arrange - _affirmationService.GetAffirmation().ReturnsError(); + _affirmationService.Get().ReturnsError(); var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.German }; @@ -101,7 +101,7 @@ public async Task Handle_WhenNoAffirmation_ShouldReturnError() // Assert result.ShouldBeError().ShouldBeOfType(); - await _affirmationService.Received(1).GetAffirmation(); + await _affirmationService.Received(1).Get(); _affirmationLanguageMapper.DidNotReceiveWithAnyArgs().Map(Arg.Any()); await _translatorClient.DidNotReceiveWithAnyArgs().Translate(Arg.Any(), Arg.Any(), Arg.Any()); } diff --git a/AffirmationGenerator.Server.Tests/Application/Queries/GetRemainingAffirmationsQueryTests.cs b/AffirmationGenerator.Server.Tests/Application/Queries/GetRemainingAffirmationsQueryTests.cs index ca06842..12e95b0 100644 --- a/AffirmationGenerator.Server.Tests/Application/Queries/GetRemainingAffirmationsQueryTests.cs +++ b/AffirmationGenerator.Server.Tests/Application/Queries/GetRemainingAffirmationsQueryTests.cs @@ -27,7 +27,7 @@ public void SetUp() public async Task Handle_ShouldReturnRemainingAffirmationsCount(int maxRequestsPerDay) { // Arrange - _affirmationService.GetRemainingAffirmationsCount().Returns(maxRequestsPerDay); + _affirmationService.GetRemainingCount().Returns(maxRequestsPerDay); // Act var result = await _query.Handle(); @@ -36,7 +36,8 @@ public async Task Handle_ShouldReturnRemainingAffirmationsCount(int maxRequestsP var response = result.ShouldBeSuccess(); response.RemainingCount.ShouldBe(maxRequestsPerDay); response.RemainingCount.ShouldBeGreaterThanOrEqualTo(0); + response.ResetInSeconds.ShouldBeGreaterThanOrEqualTo(0); - await _affirmationService.Received(1).GetRemainingAffirmationsCount(); + await _affirmationService.Received(1).GetRemainingCount(); } } diff --git a/AffirmationGenerator.Server/Application/Models/RemainingAffirmationsResponse.cs b/AffirmationGenerator.Server/Application/Models/RemainingAffirmationsResponse.cs index caec807..ba1b913 100644 --- a/AffirmationGenerator.Server/Application/Models/RemainingAffirmationsResponse.cs +++ b/AffirmationGenerator.Server/Application/Models/RemainingAffirmationsResponse.cs @@ -3,4 +3,6 @@ namespace AffirmationGenerator.Server.Application.Models; public sealed record RemainingAffirmationsResponse { public required int RemainingCount { get; init; } + + public required double ResetInSeconds { get; init; } } diff --git a/AffirmationGenerator.Server/Application/Queries/GetAffirmationQuery.cs b/AffirmationGenerator.Server/Application/Queries/GetAffirmationQuery.cs index 9d3e39b..26d43a9 100644 --- a/AffirmationGenerator.Server/Application/Queries/GetAffirmationQuery.cs +++ b/AffirmationGenerator.Server/Application/Queries/GetAffirmationQuery.cs @@ -17,7 +17,7 @@ IMapper affirmationLanguageMapper { public async Task> Handle(GetAffirmationRequest request) => await ( - from affirmation in affirmationService.GetAffirmation() + from affirmation in affirmationService.Get() from targetLanguageCode in affirmationLanguageMapper.Map(request.TargetLanguage) from translatedAffirmation in Translate(affirmation, targetLanguageCode) select ToResponse(request.TargetLanguage, translatedAffirmation) @@ -40,6 +40,6 @@ private async Task ToResponse(AffirmationLanguage targetLan { TargetLanguage = targetLanguage, Text = affirmation, - RemainingCount = await affirmationService.GetRemainingAffirmationsCount(), + RemainingCount = await affirmationService.GetRemainingCount(), }; } diff --git a/AffirmationGenerator.Server/Application/Queries/GetRemainingAffirmationsQuery.cs b/AffirmationGenerator.Server/Application/Queries/GetRemainingAffirmationsQuery.cs index 495bb31..0b48dbf 100644 --- a/AffirmationGenerator.Server/Application/Queries/GetRemainingAffirmationsQuery.cs +++ b/AffirmationGenerator.Server/Application/Queries/GetRemainingAffirmationsQuery.cs @@ -8,7 +8,12 @@ public sealed class GetRemainingAffirmationsQuery(IAffirmationService affirmatio { public async Task> Handle() { - var remainingCount = await affirmationService.GetRemainingAffirmationsCount(); - return new RemainingAffirmationsResponse { RemainingCount = remainingCount }; + var remainingCount = await affirmationService.GetRemainingCount(); + + var resetTime = await affirmationService.GetResetTime(); + + var resetInSeconds = remainingCount == 0 ? resetTime?.TotalSeconds ?? 0 : 0; + + return new RemainingAffirmationsResponse { RemainingCount = remainingCount, ResetInSeconds = resetInSeconds }; } } diff --git a/AffirmationGenerator.Server/Application/Services/Affirmation/AffirmationService.cs b/AffirmationGenerator.Server/Application/Services/Affirmation/AffirmationService.cs index 28f73ff..18f430d 100644 --- a/AffirmationGenerator.Server/Application/Services/Affirmation/AffirmationService.cs +++ b/AffirmationGenerator.Server/Application/Services/Affirmation/AffirmationService.cs @@ -22,9 +22,9 @@ IOptions clientOptions private string CacheKey => $"{ClientIpAddress}"; - public async Task> GetAffirmation() + public async Task> Get() { - var remainingAffirmations = await GetRemainingAffirmationsCount(); + var remainingAffirmations = await GetRemainingCount(); var affirmationResponse = await affirmationClient.GetAffirmation(); var affirmation = affirmationResponse.Affirmation ?? string.Empty; @@ -35,12 +35,12 @@ public async Task> GetAffirmation() return Result.Error(new AffirmationNotFound()); } - await SetRemainingAffirmationsCount(remainingAffirmations); + await SetRemainingCount(remainingAffirmations); return Result.Success(affirmation); } - public async Task GetRemainingAffirmationsCount() + public async Task GetRemainingCount() { var cachedValue = await redisClient.GetString(CacheKey); @@ -56,7 +56,9 @@ public async Task GetRemainingAffirmationsCount() return remainingCount; } - private async Task SetRemainingAffirmationsCount(int count) + public async Task GetResetTime() => await redisClient.GetKeyTtl(CacheKey); + + private async Task SetRemainingCount(int count) { if (count <= 0) return; @@ -66,6 +68,8 @@ private async Task SetRemainingAffirmationsCount(int count) if (count <= 0) count = 0; - await redisClient.SetString(CacheKey, $"{count}", TimeSpan.OneDay); + var resetTime = await GetResetTime() ?? TimeSpan.OneDay; + + await redisClient.SetString(CacheKey, $"{count}", resetTime); } } diff --git a/AffirmationGenerator.Server/Application/Services/Affirmation/IAffirmationService.cs b/AffirmationGenerator.Server/Application/Services/Affirmation/IAffirmationService.cs index 2ff11b7..54c5da5 100644 --- a/AffirmationGenerator.Server/Application/Services/Affirmation/IAffirmationService.cs +++ b/AffirmationGenerator.Server/Application/Services/Affirmation/IAffirmationService.cs @@ -4,7 +4,9 @@ namespace AffirmationGenerator.Server.Application.Services.Affirmation; public interface IAffirmationService { - Task> GetAffirmation(); + Task> Get(); - Task GetRemainingAffirmationsCount(); + Task GetRemainingCount(); + + Task GetResetTime(); } diff --git a/AffirmationGenerator.Server/Infrastructure/Redis/IRedisClient.cs b/AffirmationGenerator.Server/Infrastructure/Redis/IRedisClient.cs index 03e20a3..dce6660 100644 --- a/AffirmationGenerator.Server/Infrastructure/Redis/IRedisClient.cs +++ b/AffirmationGenerator.Server/Infrastructure/Redis/IRedisClient.cs @@ -5,4 +5,6 @@ public interface IRedisClient Task GetString(string key); Task SetString(string key, string value, TimeSpan expiration); + + Task GetKeyTtl(string key); } diff --git a/AffirmationGenerator.Server/Infrastructure/Redis/RedisClient.cs b/AffirmationGenerator.Server/Infrastructure/Redis/RedisClient.cs index 879d903..a231dce 100644 --- a/AffirmationGenerator.Server/Infrastructure/Redis/RedisClient.cs +++ b/AffirmationGenerator.Server/Infrastructure/Redis/RedisClient.cs @@ -15,5 +15,7 @@ public sealed class RedisClient(IConnectionMultiplexer redis) : IRedisClient public async Task SetString(string key, string value, TimeSpan expiration) => await Database.StringSetAsync(GetPrefixKey(key), value, expiration); + public async Task GetKeyTtl(string key) => await Database.KeyTimeToLiveAsync(GetPrefixKey(key)); + private static string GetPrefixKey(string key) => $"{nameof(RedisClient)}:{key}"; }