diff --git a/src/dotnet/QuantEngine.Application/Services/CollectionReadModelService.cs b/src/dotnet/QuantEngine.Application/Services/CollectionReadModelService.cs index beb28ab4..6b2dc85c 100644 --- a/src/dotnet/QuantEngine.Application/Services/CollectionReadModelService.cs +++ b/src/dotnet/QuantEngine.Application/Services/CollectionReadModelService.cs @@ -13,9 +13,31 @@ public sealed class CollectionReadModelService : ICollectionReadModelService } public Task GetDashboardStateAsync() => _repository.GetDashboardStateAsync(); - public Task> GetRecentRunsAsync(int limit = 20) => _repository.GetRecentRunsAsync(limit); - public Task> GetRunSnapshotsAsync(string runId) => _repository.GetRunSnapshotsAsync(runId); - public Task> GetRunErrorsAsync(string runId, int limit = 50) => _repository.GetRunErrorsAsync(runId, limit); - public Task> GetLatestSnapshotsForTickerAsync(string ticker, int limit = 10) => _repository.GetLatestSnapshotsForTickerAsync(ticker, limit); + + public Task> GetRecentRunsAsync(int limit = 20) + => _repository.GetRecentRunsAsync(NormalizeLimit(limit, 1, 200)); + + public Task> GetRunSnapshotsAsync(string runId) + => _repository.GetRunSnapshotsAsync(RequireValue(runId, nameof(runId))); + + public Task> GetRunErrorsAsync(string runId, int limit = 50) + => _repository.GetRunErrorsAsync(RequireValue(runId, nameof(runId)), NormalizeLimit(limit, 1, 200)); + + public Task> GetLatestSnapshotsForTickerAsync(string ticker, int limit = 10) + => _repository.GetLatestSnapshotsForTickerAsync(RequireValue(ticker, nameof(ticker)), NormalizeLimit(limit, 1, 100)); + public Task> GetPriceHistorySummaryAsync() => _repository.GetPriceHistorySummaryAsync(); + + private static string RequireValue(string value, string parameterName) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Value is required.", parameterName); + } + + return value.Trim(); + } + + private static int NormalizeLimit(int limit, int min, int max) + => Math.Clamp(limit, min, max); } diff --git a/src/dotnet/QuantEngine.Core.Tests/CollectionReadModelServiceTests.cs b/src/dotnet/QuantEngine.Core.Tests/CollectionReadModelServiceTests.cs new file mode 100644 index 00000000..143f22c8 --- /dev/null +++ b/src/dotnet/QuantEngine.Core.Tests/CollectionReadModelServiceTests.cs @@ -0,0 +1,44 @@ +using Moq; +using QuantEngine.Application.Services; +using QuantEngine.Core.Interfaces; + +namespace QuantEngine.Core.Tests; + +public class CollectionReadModelServiceTests +{ + [Fact] + public async Task GetRecentRunsAsync_ClampsLimitToUpperBound() + { + var repo = new Mock(MockBehavior.Strict); + repo.Setup(r => r.GetRecentRunsAsync(200)).ReturnsAsync([]); + + var service = new CollectionReadModelService(repo.Object); + + var result = await service.GetRecentRunsAsync(999); + + Assert.Empty(result); + repo.VerifyAll(); + } + + [Fact] + public async Task GetLatestSnapshotsForTickerAsync_TrimsTickerAndClampsLimit() + { + var repo = new Mock(MockBehavior.Strict); + repo.Setup(r => r.GetLatestSnapshotsForTickerAsync("005930", 100)).ReturnsAsync([]); + + var service = new CollectionReadModelService(repo.Object); + + var result = await service.GetLatestSnapshotsForTickerAsync(" 005930 ", 999); + + Assert.Empty(result); + repo.VerifyAll(); + } + + [Fact] + public async Task GetRunErrorsAsync_RejectsEmptyRunId() + { + var service = new CollectionReadModelService(new Mock().Object); + + await Assert.ThrowsAsync(() => service.GetRunErrorsAsync(" ", 10)); + } +}