diff --git a/backend/src/Logitar.Master.Application/BadRequestException.cs b/backend/src/Logitar.Master.Application/BadRequestException.cs new file mode 100644 index 0000000..90c7c08 --- /dev/null +++ b/backend/src/Logitar.Master.Application/BadRequestException.cs @@ -0,0 +1,12 @@ +using Logitar.Master.Contracts.Errors; + +namespace Logitar.Master.Application; + +public abstract class BadRequestException : Exception +{ + public abstract Error Error { get; } + + protected BadRequestException(string? message = null, Exception? innerException = null) : base(message, innerException) + { + } +} diff --git a/backend/src/Logitar.Master.Application/Projects/Queries/ReadProjectQuery.cs b/backend/src/Logitar.Master.Application/Projects/Queries/ReadProjectQuery.cs new file mode 100644 index 0000000..0652bac --- /dev/null +++ b/backend/src/Logitar.Master.Application/Projects/Queries/ReadProjectQuery.cs @@ -0,0 +1,6 @@ +using Logitar.Master.Contracts.Projects; +using MediatR; + +namespace Logitar.Master.Application.Projects.Queries; + +public record ReadProjectQuery(Guid? Id, string? UniqueKey) : IRequest; diff --git a/backend/src/Logitar.Master.Application/Projects/Queries/ReadProjectQueryHandler.cs b/backend/src/Logitar.Master.Application/Projects/Queries/ReadProjectQueryHandler.cs new file mode 100644 index 0000000..64d981b --- /dev/null +++ b/backend/src/Logitar.Master.Application/Projects/Queries/ReadProjectQueryHandler.cs @@ -0,0 +1,44 @@ +using Logitar.Master.Contracts.Projects; +using MediatR; + +namespace Logitar.Master.Application.Projects.Queries; + +internal class ReadProjectQueryHandler : IRequestHandler +{ + private readonly IProjectQuerier _projectQuerier; + + public ReadProjectQueryHandler(IProjectQuerier projectQuerier) + { + _projectQuerier = projectQuerier; + } + + public async Task Handle(ReadProjectQuery query, CancellationToken cancellationToken) + { + Dictionary projects = new(capacity: 2); + + if (query.Id.HasValue) + { + Project? project = await _projectQuerier.ReadAsync(query.Id.Value, cancellationToken); + if (project != null) + { + projects[project.Id] = project; + } + } + + if (!string.IsNullOrWhiteSpace(query.UniqueKey)) + { + Project? project = await _projectQuerier.ReadAsync(query.UniqueKey, cancellationToken); + if (project != null) + { + projects[project.Id] = project; + } + } + + if (projects.Count > 1) + { + throw TooManyResultsException.ExpectedSingle(projects.Count); + } + + return projects.Values.SingleOrDefault(); + } +} diff --git a/backend/src/Logitar.Master.Application/TooManyResultsException.cs b/backend/src/Logitar.Master.Application/TooManyResultsException.cs new file mode 100644 index 0000000..8c1f59b --- /dev/null +++ b/backend/src/Logitar.Master.Application/TooManyResultsException.cs @@ -0,0 +1,57 @@ +using Logitar.Master.Contracts.Errors; + +namespace Logitar.Master.Application; + +public class TooManyResultsException : BadRequestException +{ + private const string ErrorMessage = "There are too many results."; + + public string TypeName + { + get => (string)Data[nameof(TypeName)]!; + private set => Data[nameof(TypeName)] = value; + } + public int ExpectedCount + { + get => (int)Data[nameof(ExpectedCount)]!; + private set => Data[nameof(ExpectedCount)] = value; + } + public int ActualCount + { + get => (int)Data[nameof(ActualCount)]!; + private set => Data[nameof(ActualCount)] = value; + } + + public override Error Error + { + get + { + Error error = new(this.GetErrorCode(), ErrorMessage); + error.Add(nameof(ExpectedCount), ExpectedCount.ToString()); + error.Add(nameof(ActualCount), ActualCount.ToString()); + return error; + } + } + + public TooManyResultsException(Type type, int expectedCount, int actualCount) : base(BuildMessage(type, expectedCount, actualCount)) + { + TypeName = type.GetNamespaceQualifiedName(); + ExpectedCount = expectedCount; + ActualCount = actualCount; + } + + private static string BuildMessage(Type type, int expectedCount, int actualCount) => new ErrorMessageBuilder(ErrorMessage) + .AddData(nameof(TypeName), type.GetNamespaceQualifiedName()) + .AddData(nameof(ExpectedCount), expectedCount) + .AddData(nameof(ActualCount), actualCount) + .Build(); +} + +public class TooManyResultsException : TooManyResultsException +{ + public TooManyResultsException(int expectedCount, int actualCount) : base(typeof(T), expectedCount, actualCount) + { + } + + public static TooManyResultsException ExpectedSingle(int actualCount) => new(expectedCount: 1, actualCount); +} diff --git a/backend/src/Logitar.Master/Controllers/ProjectController.cs b/backend/src/Logitar.Master/Controllers/ProjectController.cs index d47a673..a3b237a 100644 --- a/backend/src/Logitar.Master/Controllers/ProjectController.cs +++ b/backend/src/Logitar.Master/Controllers/ProjectController.cs @@ -1,4 +1,5 @@ using Logitar.Master.Application.Projects.Commands; +using Logitar.Master.Application.Projects.Queries; using Logitar.Master.Contracts.Projects; using Logitar.Master.Extensions; using MediatR; @@ -24,5 +25,19 @@ public async Task> CreateAsync([FromBody] CreateProjectPay return Created(BuildLocation(project), project); } + [HttpGet("{id}")] + public async Task> ReadAsync(Guid id, CancellationToken cancellationToken) + { + Project? project = await _sender.Send(new ReadProjectQuery(id, UniqueKey: null), cancellationToken); + return project == null ? NotFound() : Ok(project); + } + + [HttpGet("key:{uniqueKey}")] + public async Task> ReadAsync(string uniqueKey, CancellationToken cancellationToken) + { + Project? project = await _sender.Send(new ReadProjectQuery(Id: null, uniqueKey), cancellationToken); + return project == null ? NotFound() : Ok(project); + } + private Uri BuildLocation(Project project) => HttpContext.BuildLocation("projects/{id}", new Dictionary { ["id"] = project.Id.ToString() }); } diff --git a/backend/src/Logitar.Master/Filters/ExceptionHandling.cs b/backend/src/Logitar.Master/Filters/ExceptionHandling.cs index 3748bd4..971a0bc 100644 --- a/backend/src/Logitar.Master/Filters/ExceptionHandling.cs +++ b/backend/src/Logitar.Master/Filters/ExceptionHandling.cs @@ -15,6 +15,11 @@ public override void OnException(ExceptionContext context) context.Result = new BadRequestObjectResult(BuildError(validation)); context.ExceptionHandled = true; } + else if (context.Exception is BadRequestException badRequest) + { + context.Result = new BadRequestObjectResult(badRequest.Error); + context.ExceptionHandled = true; + } else if (context.Exception is ConflictException conflict) { context.Result = new ConflictObjectResult(conflict.Error); diff --git a/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Queries/ReadProjectQueryTests.cs b/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Queries/ReadProjectQueryTests.cs new file mode 100644 index 0000000..93ad222 --- /dev/null +++ b/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Queries/ReadProjectQueryTests.cs @@ -0,0 +1,64 @@ +using Logitar.Master.Contracts.Projects; +using Logitar.Master.Domain.Projects; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Master.Application.Projects.Queries; + +[Trait(Traits.Category, Categories.Integration)] +public class ReadProjectQueryTests : IntegrationTests +{ + private readonly IProjectRepository _projectRepository; + + private readonly ProjectAggregate _master; + private readonly ProjectAggregate _portal; + + public ReadProjectQueryTests() : base() + { + _projectRepository = ServiceProvider.GetRequiredService(); + + _master = new(new UniqueKeyUnit("MASTER")); + _portal = new(new UniqueKeyUnit("PORTAL")); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + await _projectRepository.SaveAsync([_master, _portal]); + } + + [Fact(DisplayName = "It should return null when no project was found.")] + public async Task It_should_return_null_when_no_project_was_found() + { + ReadProjectQuery query = new(Id: Guid.Empty, UniqueKey: "test"); + Project? project = await Mediator.Send(query); + Assert.Null(project); + } + + [Fact(DisplayName = "It should return the project found by ID.")] + public async Task It_should_return_the_project_found_by_Id() + { + ReadProjectQuery query = new(Id: _master.Id.ToGuid(), UniqueKey: null); + Project? project = await Mediator.Send(query); + Assert.NotNull(project); + Assert.Equal(_master.Id.ToGuid(), project.Id); + } + + [Fact(DisplayName = "It should return the project found by unique key.")] + public async Task It_should_return_the_project_found_by_unique_key() + { + ReadProjectQuery query = new(Id: null, UniqueKey: " portal "); + Project? project = await Mediator.Send(query); + Assert.NotNull(project); + Assert.Equal(_portal.Id.ToGuid(), project.Id); + } + + [Fact(DisplayName = "It should throw TooManyResultsException when many projects were found.")] + public async Task It_should_throw_TooManyResultsException_when_many_projects_were_found() + { + ReadProjectQuery query = new(_master.Id.ToGuid(), UniqueKey: " portal "); + var exception = await Assert.ThrowsAsync>(async () => await Mediator.Send(query)); + Assert.Equal(1, exception.ExpectedCount); + Assert.Equal(2, exception.ActualCount); + } +}