diff --git a/NUGET.md b/NUGET.md index d3f2113..2b67180 100644 --- a/NUGET.md +++ b/NUGET.md @@ -18,6 +18,8 @@ dotnet new cleanminimalapi --name {YOUR_SOLUTION_NAMESPACE} --au "{YOU_AUTHORS_N ## Connect and Support -If you like this, or want to checkout my other work, please connect with me on [LinkedIn](https://www.linkedin.com/in/stphnwlsh), [Twitter](https://twitter.com/stphnwlsh) or [GitHub](https://github.com/stphnwlsh), and consider supporting me at [Buy Me a Coffee]. +If you like this, or want to checkout my other work, please connect with me on [LinkedIn](https://www.linkedin.com/in/stphnwlsh), and/or follow me on [Medium](https://stphnwlsh.medium.com) or [GitHub](https://github.com/stphnwlsh). -[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-1.svg)](https://www.buymeacoffee.com/stphnwlsh) +If you want to see more updates or more projects then please support me at [GitHub Sponsors](https://github.com/stphnwlsh) or [Buy Me A Coffee](https://www.buymeacoffee.com/stphnwlsh) + +![Please Sponsor Me](docs/sponsor.jpg) diff --git a/README.md b/README.md index 4f2a81e..a3d9ec6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/stphnwlsh/cleanminimalapi/build-pipeline.yml?label=Build%20Pipeline%20&logo=github&style=for-the-badge)](https://github.com/stphnwlsh/CleanMinimalApi/actions/workflows/build-pipeline.yml) [![Codecov](https://img.shields.io/codecov/c/github/stphnwlsh/CleanMinimalApi?label=Code%20Coverage&logo=codecov&logoColor=white&style=for-the-badge)](https://codecov.io/gh/stphnwlsh/CleanMinimalApi) -![Nuget](https://img.shields.io/nuget/v/CleanMinimalApi.Template?label=nuget%20template&logo=nuget&logoColor=white&style=for-the-badge) +[![Nuget](https://img.shields.io/nuget/v/CleanMinimalApi.Template?label=nuget%20template&logo=nuget&logoColor=white&style=for-the-badge)](https://www.nuget.org/packages/CleanMinimalApi.Template/) +[![GitHub Sponsors](https://img.shields.io/static/v1?label=GitHub%20Sponsors&message=$1&logo=githubsponsors&logoColor=white&color=ea4aaa&style=for-the-badge)](https://github.com/sponsors/stphnwlsh/sponsorships?sponsor=stphnwlsh&tier_id=333950) +[![Buy Me A Coffee](https://img.shields.io/static/v1?label=Buy%20Me%20A%20Coffee&message=$1&logo=buymeacoffee&logoColor=white&color=ffdd00&style=for-the-badge)](https://www.buymeacoffee.com/stphnwlsh) This is a template API using a streamlined version of [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) alongside .NET's [Minimal APIs](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-7.0). @@ -73,9 +75,12 @@ This sample would not have been possible without gaining inspiration from the fo - [Damian Edwards - Minimal API Playground](https://github.com/DamianEdwards/MinimalApiPlayground) - [Scott Hanselman - Minimal APIs in .NET 6 but where are the Unit Tests?](https://www.hanselman.com/blog/minimal-apis-in-net-6-but-where-are-the-unit-tests) - [Andrew Lock - Reducing log verbosity with Serilog RequestLogging](https://andrewlock.net/using-serilog-aspnetcore-in-asp-net-core-3-reducing-log-verbosity/) +- [Ben Foster - Minimal API validation with ASP.NET 7.0 Endpoint Filters](https://benfoster.io/blog/minimal-api-validation-endpoint-filters/) ## Connect and Support -If you like this, or want to checkout my other work, please connect with me on [LinkedIn](https://www.linkedin.com/in/stphnwlsh), [Medium](https://stphnwlsh.medium.com) or [GitHub](https://github.com/stphnwlsh), and consider supporting me by sponsoring the project. +If you like this, or want to checkout my other work, please connect with me on [LinkedIn](https://www.linkedin.com/in/stphnwlsh), and/or follow me on [Medium](https://stphnwlsh.medium.com) or [GitHub](https://github.com/stphnwlsh). -[!["GitHub Sponsor Me"](https://github.blog/wp-content/uploads/2019/05/mona-heart-featured.png)](https://github.com/sponsors/stphnwlsh) +If you want to see more updates or more projects then please support me at [GitHub Sponsors](https://github.com/stphnwlsh) or [Buy Me A Coffee](https://www.buymeacoffee.com/stphnwlsh) + +![Please Sponsor Me](docs/sponsor.jpg) diff --git a/codecov.yml b/codecov.yml index a973dba..0edb813 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,4 +3,4 @@ coverage: project: default: target: 100% - threshold: 3% + threshold: 2% diff --git a/docs/sponsor.jpg b/docs/sponsor.jpg new file mode 100644 index 0000000..ca06ad1 Binary files /dev/null and b/docs/sponsor.jpg differ diff --git a/src/Application/Authors/Entities/Author.cs b/src/Application/Authors/Entities/Author.cs index a75abdc..40ae919 100644 --- a/src/Application/Authors/Entities/Author.cs +++ b/src/Application/Authors/Entities/Author.cs @@ -1,13 +1,5 @@ namespace CleanMinimalApi.Application.Authors.Entities; -using Application.Common.Entities; using Application.Reviews.Entities; -public record Author : Entity -{ - public string FirstName { get; set; } - - public string LastName { get; set; } - - public ICollection Reviews { get; set; } -} +public record Author(Guid Id, string FirstName, string LastName, ICollection Reviews = null); diff --git a/src/Application/Authors/Entities/ReviewAuthor.cs b/src/Application/Authors/Entities/ReviewAuthor.cs index 202cd19..50b8963 100644 --- a/src/Application/Authors/Entities/ReviewAuthor.cs +++ b/src/Application/Authors/Entities/ReviewAuthor.cs @@ -1,10 +1,3 @@ namespace CleanMinimalApi.Application.Authors.Entities; -using Application.Common.Entities; - -public record ReviewAuthor : Entity -{ - public string FirstName { get; set; } - - public string LastName { get; set; } -} +public record ReviewAuthor(Guid Id, string FirstName, string LastName); diff --git a/src/Application/Common/Entities/Entity.cs b/src/Application/Common/Entities/Entity.cs deleted file mode 100644 index 32b69f1..0000000 --- a/src/Application/Common/Entities/Entity.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CleanMinimalApi.Application.Common.Entities; - -public abstract record Entity -{ - public Guid Id { get; init; } - - public DateTime DateCreated { get; init; } - - public DateTime DateModified { get; init; } -} diff --git a/src/Application/Movies/Entities/Movie.cs b/src/Application/Movies/Entities/Movie.cs index db9ea18..9c4ae4f 100644 --- a/src/Application/Movies/Entities/Movie.cs +++ b/src/Application/Movies/Entities/Movie.cs @@ -1,11 +1,5 @@ namespace CleanMinimalApi.Application.Movies.Entities; -using Application.Common.Entities; using Application.Reviews.Entities; -public record Movie : Entity -{ - public string Title { get; init; } - - public ICollection Reviews { get; init; } -} +public record Movie(Guid Id, string Title, ICollection Reviews = null); diff --git a/src/Application/Movies/Entities/ReviewMovie.cs b/src/Application/Movies/Entities/ReviewMovie.cs deleted file mode 100644 index 942ab02..0000000 --- a/src/Application/Movies/Entities/ReviewMovie.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace CleanMinimalApi.Application.Movies.Entities; - -using Application.Common.Entities; - -public record ReviewMovie : Entity -{ - public string Title { get; init; } -} diff --git a/src/Application/Movies/Entities/ReviewedMovie.cs b/src/Application/Movies/Entities/ReviewedMovie.cs new file mode 100644 index 0000000..626698c --- /dev/null +++ b/src/Application/Movies/Entities/ReviewedMovie.cs @@ -0,0 +1,3 @@ +namespace CleanMinimalApi.Application.Movies.Entities; + +public record ReviewedMovie(Guid Id, string Title); diff --git a/src/Application/Reviews/Commands/CreateReview/CreateReviewHandler.cs b/src/Application/Reviews/Commands/CreateReview/CreateReviewHandler.cs index e74b6ce..24cc44f 100644 --- a/src/Application/Reviews/Commands/CreateReview/CreateReviewHandler.cs +++ b/src/Application/Reviews/Commands/CreateReview/CreateReviewHandler.cs @@ -26,6 +26,7 @@ public async Task Handle(CreateReviewCommand request, CancellationToken NotFoundException.Throw(EntityType.Movie); } - return await reviewsRepository.CreateReview(request.AuthorId, request.MovieId, request.Stars, cancellationToken); + return await reviewsRepository + .CreateReview(request.AuthorId, request.MovieId, request.Stars, cancellationToken); } } diff --git a/src/Application/Reviews/Commands/UpdateReview/UpdateReviewHandler.cs b/src/Application/Reviews/Commands/UpdateReview/UpdateReviewHandler.cs index f2ca8ea..4b89c37 100644 --- a/src/Application/Reviews/Commands/UpdateReview/UpdateReviewHandler.cs +++ b/src/Application/Reviews/Commands/UpdateReview/UpdateReviewHandler.cs @@ -30,6 +30,7 @@ public async Task Handle(UpdateReviewCommand request, CancellationToken ca NotFoundException.Throw(EntityType.Movie); } - return await reviewsRepository.UpdateReview(request.Id, request.AuthorId, request.MovieId, request.Stars, cancellationToken); + return await reviewsRepository + .UpdateReview(request.Id, request.AuthorId, request.MovieId, request.Stars, cancellationToken); } } diff --git a/src/Application/Reviews/Entities/Review.cs b/src/Application/Reviews/Entities/Review.cs index 2ed4432..be835ee 100644 --- a/src/Application/Reviews/Entities/Review.cs +++ b/src/Application/Reviews/Entities/Review.cs @@ -1,14 +1,10 @@ namespace CleanMinimalApi.Application.Reviews.Entities; using Application.Authors.Entities; -using Application.Common.Entities; using Application.Movies.Entities; -public record Review : Entity -{ - public int Stars { get; init; } - - public ReviewMovie ReviewedMovie { get; init; } - - public ReviewAuthor ReviewAuthor { get; init; } -} +public record Review( + Guid Id, + int Stars, + ReviewedMovie ReviewedMovie = null, + ReviewAuthor ReviewAuthor = null); diff --git a/src/Application/Reviews/IReviewsRepository.cs b/src/Application/Reviews/IReviewsRepository.cs index 4850e69..76bfbc4 100644 --- a/src/Application/Reviews/IReviewsRepository.cs +++ b/src/Application/Reviews/IReviewsRepository.cs @@ -5,7 +5,11 @@ namespace CleanMinimalApi.Application.Reviews; public interface IReviewsRepository { - Task CreateReview(Guid authorId, Guid movieId, int stars, CancellationToken cancellationToken); + Task CreateReview( + Guid authorId, + Guid movieId, + int stars, + CancellationToken cancellationToken); Task DeleteReview(Guid id, CancellationToken cancellationToken); @@ -15,5 +19,10 @@ public interface IReviewsRepository Task ReviewExists(Guid id, CancellationToken cancellationToken); - Task UpdateReview(Guid id, Guid authorId, Guid movieId, int stars, CancellationToken cancellationToken); + Task UpdateReview( + Guid id, + Guid authorId, + Guid movieId, + int stars, + CancellationToken cancellationToken); } diff --git a/src/Application/Versions/Queries/GetVersion/GetVersionHandler.cs b/src/Application/Versions/Queries/GetVersion/GetVersionHandler.cs index ac28d35..ea99e9b 100644 --- a/src/Application/Versions/Queries/GetVersion/GetVersionHandler.cs +++ b/src/Application/Versions/Queries/GetVersion/GetVersionHandler.cs @@ -10,10 +10,14 @@ public class GetVersionHandler : IRequestHandler { public Task Handle(GetVersionQuery request, CancellationToken cancellationToken) { + var assembly = Assembly.GetEntryAssembly(); + var version = new Version { - FileVersion = $"{Assembly.GetEntryAssembly()?.GetCustomAttribute()?.Version}", - InformationalVersion = $"{Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion}" + FileVersion = + $"{assembly?.GetCustomAttribute()?.Version}", + InformationalVersion = + $"{assembly?.GetCustomAttribute()?.InformationalVersion}" }; return Task.FromResult(version); diff --git a/src/Infrastructure/Databases/MoviesReviews/EntityFrameworkMovieReviewsRepository.cs b/src/Infrastructure/Databases/MoviesReviews/EntityFrameworkMovieReviewsRepository.cs index 5132717..525559e 100644 --- a/src/Infrastructure/Databases/MoviesReviews/EntityFrameworkMovieReviewsRepository.cs +++ b/src/Infrastructure/Databases/MoviesReviews/EntityFrameworkMovieReviewsRepository.cs @@ -20,7 +20,10 @@ internal class EntityFrameworkMovieReviewsRepository : IAuthorsRepository, IMovi private readonly TimeProvider timeProvider; private readonly IMapper mapper; - public EntityFrameworkMovieReviewsRepository(MovieReviewsDbContext context, TimeProvider timeProvider, IMapper mapper) + public EntityFrameworkMovieReviewsRepository( + MovieReviewsDbContext context, + TimeProvider timeProvider, + IMapper mapper) { this.context = context; this.timeProvider = timeProvider; @@ -38,14 +41,22 @@ public EntityFrameworkMovieReviewsRepository(MovieReviewsDbContext context, Time public virtual async Task> GetAuthors(CancellationToken cancellationToken) { - var authors = await this.context.Authors.Include(a => a.Reviews).ThenInclude(r => r.ReviewedMovie).AsNoTracking().ToListAsync(cancellationToken); + var authors = await this.context.Authors + .Include(a => a.Reviews) + .ThenInclude(r => r.ReviewedMovie) + .AsNoTracking() + .ToListAsync(cancellationToken); return this.mapper.Map>(authors); } public virtual async Task GetAuthorById(Guid id, CancellationToken cancellationToken) { - var author = await this.context.Authors.Where(r => r.Id == id).Include(a => a.Reviews).ThenInclude(r => r.ReviewedMovie).AsNoTracking().FirstOrDefaultAsync(cancellationToken); + var author = await this.context.Authors + .Where(r => r.Id == id).Include(a => a.Reviews) + .ThenInclude(r => r.ReviewedMovie) + .AsNoTracking() + .FirstOrDefaultAsync(cancellationToken); return this.mapper.Map(author); ; @@ -62,14 +73,23 @@ public virtual async Task AuthorExists(Guid id, CancellationToken cancella public virtual async Task> GetMovies(CancellationToken cancellationToken) { - var result = await this.context.Movies.Include(m => m.Reviews).ThenInclude(r => r.ReviewAuthor).AsNoTracking().ToListAsync(cancellationToken); + var result = await this.context.Movies + .Include(m => m.Reviews) + .ThenInclude(r => r.ReviewAuthor) + .AsNoTracking() + .ToListAsync(cancellationToken); return this.mapper.Map>(result); } public virtual async Task GetMovieById(Guid id, CancellationToken cancellationToken) { - var result = await this.context.Movies.Where(r => r.Id == id).Include(m => m.Reviews).ThenInclude(r => r.ReviewAuthor).AsNoTracking().FirstOrDefaultAsync(cancellationToken); + var result = await this.context.Movies + .Where(r => r.Id == id) + .Include(m => m.Reviews) + .ThenInclude(r => r.ReviewAuthor) + .AsNoTracking() + .FirstOrDefaultAsync(cancellationToken); return this.mapper.Map(result); } @@ -83,7 +103,11 @@ public virtual async Task MovieExists(Guid id, CancellationToken cancellat #region Reviews - public async Task CreateReview(Guid authorId, Guid movieId, int stars, CancellationToken cancellationToken) + public async Task CreateReview( + Guid authorId, + Guid movieId, + int stars, + CancellationToken cancellationToken) { var review = new Review { @@ -98,7 +122,12 @@ public async Task CreateReview(Guid authorId, Guid movieId, i _ = await this.context.SaveChangesAsync(cancellationToken); - var result = await this.context.Reviews.Where(r => r.Id == id).Include(r => r.ReviewAuthor).Include(r => r.ReviewedMovie).AsNoTracking().FirstAsync(cancellationToken); + var result = await this.context.Reviews + .Where(r => r.Id == id) + .Include(r => r.ReviewAuthor) + .Include(r => r.ReviewedMovie) + .AsNoTracking() + .FirstAsync(cancellationToken); return this.mapper.Map(result); } @@ -120,14 +149,23 @@ public async Task DeleteReview(Guid id, CancellationToken cancellationToke public async Task> GetReviews(CancellationToken cancellationToken) { - var result = await this.context.Reviews.Include(r => r.ReviewAuthor).Include(r => r.ReviewedMovie).AsNoTracking().ToListAsync(cancellationToken); + var result = await this.context.Reviews + .Include(r => r.ReviewAuthor) + .Include(r => r.ReviewedMovie) + .AsNoTracking() + .ToListAsync(cancellationToken); return this.mapper.Map>(result); } public async Task GetReviewById(Guid id, CancellationToken cancellationToken) { - var result = await this.context.Reviews.Where(r => r.Id == id).Include(r => r.ReviewAuthor).Include(r => r.ReviewedMovie).AsNoTracking().FirstOrDefaultAsync(cancellationToken); + var result = await this.context.Reviews + .Where(r => r.Id == id) + .Include(r => r.ReviewAuthor) + .Include(r => r.ReviewedMovie) + .AsNoTracking() + .FirstOrDefaultAsync(cancellationToken); return this.mapper.Map(result); } @@ -137,7 +175,12 @@ public async Task ReviewExists(Guid id, CancellationToken cancellationToke return await this.context.Reviews.AnyAsync(r => r.Id == id, cancellationToken); } - public async Task UpdateReview(Guid id, Guid authorId, Guid movieId, int stars, CancellationToken cancellationToken) + public async Task UpdateReview( + Guid id, + Guid authorId, + Guid movieId, + int stars, + CancellationToken cancellationToken) { try { diff --git a/src/Infrastructure/Databases/MoviesReviews/Mapping/AuthorMappingProfile.cs b/src/Infrastructure/Databases/MoviesReviews/Mapping/AuthorMappingProfile.cs index 9052609..8c73f36 100644 --- a/src/Infrastructure/Databases/MoviesReviews/Mapping/AuthorMappingProfile.cs +++ b/src/Infrastructure/Databases/MoviesReviews/Mapping/AuthorMappingProfile.cs @@ -8,9 +8,15 @@ internal class AuthorMappingProfile : Profile { public AuthorMappingProfile() { - _ = this.CreateMap() + _ = this.CreateMap() + .ForMember(d => d.DateCreated, o => o.Ignore()) + .ForMember(d => d.DateModified, o => o.Ignore()) .ReverseMap(); - _ = this.CreateMap(); + _ = this.CreateMap() + .ForMember(d => d.Reviews, o => o.Ignore()) + .ForMember(d => d.DateCreated, o => o.Ignore()) + .ForMember(d => d.DateModified, o => o.Ignore()) + .ReverseMap(); } } diff --git a/src/Infrastructure/Databases/MoviesReviews/Mapping/EntitiyMappingProfile.cs b/src/Infrastructure/Databases/MoviesReviews/Mapping/EntitiyMappingProfile.cs deleted file mode 100644 index 19099ba..0000000 --- a/src/Infrastructure/Databases/MoviesReviews/Mapping/EntitiyMappingProfile.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace CleanMinimalApi.Infrastructure.Databases.MoviesReviews.Mapping; - -using AutoMapper; -using Application = Application.Common.Entities; -using Infrastructure = Models; - -internal class EntitiyMappingProfile : Profile -{ - public EntitiyMappingProfile() - { - _ = this.CreateMap() - .ReverseMap(); - } -} diff --git a/src/Infrastructure/Databases/MoviesReviews/Mapping/MovieMappingProfile.cs b/src/Infrastructure/Databases/MoviesReviews/Mapping/MovieMappingProfile.cs index d232fb3..79c3cfb 100644 --- a/src/Infrastructure/Databases/MoviesReviews/Mapping/MovieMappingProfile.cs +++ b/src/Infrastructure/Databases/MoviesReviews/Mapping/MovieMappingProfile.cs @@ -8,9 +8,15 @@ internal class MovieMappingProfile : Profile { public MovieMappingProfile() { - _ = this.CreateMap() + _ = this.CreateMap() + .ForMember(d => d.DateCreated, o => o.Ignore()) + .ForMember(d => d.DateModified, o => o.Ignore()) .ReverseMap(); - _ = this.CreateMap(); + _ = this.CreateMap() + .ForMember(d => d.Reviews, o => o.Ignore()) + .ForMember(d => d.DateCreated, o => o.Ignore()) + .ForMember(d => d.DateModified, o => o.Ignore()) + .ReverseMap(); } } diff --git a/src/Infrastructure/Databases/MoviesReviews/Mapping/ReviewMappingProfile.cs b/src/Infrastructure/Databases/MoviesReviews/Mapping/ReviewMappingProfile.cs index d08931f..6b43e1f 100644 --- a/src/Infrastructure/Databases/MoviesReviews/Mapping/ReviewMappingProfile.cs +++ b/src/Infrastructure/Databases/MoviesReviews/Mapping/ReviewMappingProfile.cs @@ -10,7 +10,11 @@ public ReviewMappingProfile() { _ = this.CreateMap() .ForMember(d => d.ReviewAuthorId, o => o.Ignore()) + .ForMember(d => d.ReviewAuthor, o => o.MapFrom(s => s.ReviewAuthor)) .ForMember(d => d.ReviewedMovieId, o => o.Ignore()) + .ForMember(d => d.ReviewedMovie, o => o.MapFrom(s => s.ReviewedMovie)) + .ForMember(d => d.DateCreated, o => o.Ignore()) + .ForMember(d => d.DateModified, o => o.Ignore()) .ReverseMap(); } } diff --git a/src/Infrastructure/Databases/MoviesReviews/Models/Author.cs b/src/Infrastructure/Databases/MoviesReviews/Models/Author.cs index 3bebe67..3a3744f 100644 --- a/src/Infrastructure/Databases/MoviesReviews/Models/Author.cs +++ b/src/Infrastructure/Databases/MoviesReviews/Models/Author.cs @@ -1,13 +1,13 @@ namespace CleanMinimalApi.Infrastructure.Databases.MoviesReviews.Models; -using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics.CodeAnalysis; +[ExcludeFromCodeCoverage] internal record Author : Entity { public string FirstName { get; init; } public string LastName { get; init; } - [InverseProperty("ReviewAuthor")] public ICollection Reviews { get; init; } } diff --git a/src/Infrastructure/Databases/MoviesReviews/Models/Entity.cs b/src/Infrastructure/Databases/MoviesReviews/Models/Entity.cs index fc6cfab..5634632 100644 --- a/src/Infrastructure/Databases/MoviesReviews/Models/Entity.cs +++ b/src/Infrastructure/Databases/MoviesReviews/Models/Entity.cs @@ -1,5 +1,8 @@ namespace CleanMinimalApi.Infrastructure.Databases.MoviesReviews.Models; +using System.Diagnostics.CodeAnalysis; + +[ExcludeFromCodeCoverage] internal abstract record Entity { public Guid Id { get; init; } diff --git a/src/Infrastructure/Databases/MoviesReviews/Models/Movie.cs b/src/Infrastructure/Databases/MoviesReviews/Models/Movie.cs index 312e94a..4b56406 100644 --- a/src/Infrastructure/Databases/MoviesReviews/Models/Movie.cs +++ b/src/Infrastructure/Databases/MoviesReviews/Models/Movie.cs @@ -1,11 +1,11 @@ namespace CleanMinimalApi.Infrastructure.Databases.MoviesReviews.Models; -using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics.CodeAnalysis; +[ExcludeFromCodeCoverage] internal record Movie : Entity { public string Title { get; init; } - [InverseProperty("ReviewedMovie")] public ICollection Reviews { get; init; } } diff --git a/src/Infrastructure/Databases/MoviesReviews/Models/Review.cs b/src/Infrastructure/Databases/MoviesReviews/Models/Review.cs index c50fbd0..98a7cab 100644 --- a/src/Infrastructure/Databases/MoviesReviews/Models/Review.cs +++ b/src/Infrastructure/Databases/MoviesReviews/Models/Review.cs @@ -1,7 +1,9 @@ namespace CleanMinimalApi.Infrastructure.Databases.MoviesReviews.Models; using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics.CodeAnalysis; +[ExcludeFromCodeCoverage] internal record Review : Entity { public int Stars { get; set; } diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index bd6fbe3..7210e27 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -12,15 +12,19 @@ public static class DependencyInjection { public static IServiceCollection AddInfrastructure(this IServiceCollection services) { - _ = services.AddDbContext(options => options.UseInMemoryDatabase($"Movies-{Guid.NewGuid()}"), ServiceLifetime.Singleton); + _ = services.AddDbContext(options => + options.UseInMemoryDatabase($"Movies-{Guid.NewGuid()}"), ServiceLifetime.Singleton); _ = services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); _ = services.AddSingleton(); - _ = services.AddSingleton(p => p.GetRequiredService()); - _ = services.AddSingleton(x => x.GetRequiredService()); - _ = services.AddSingleton(x => x.GetRequiredService()); + _ = services.AddSingleton(p => + p.GetRequiredService()); + _ = services.AddSingleton(x => + x.GetRequiredService()); + _ = services.AddSingleton(x => + x.GetRequiredService()); _ = services.AddSingleton(TimeProvider.System); diff --git a/src/Presentation/Endpoints/AuthorsEndpoints.cs b/src/Presentation/Endpoints/AuthorsEndpoints.cs index 2af2761..b994322 100644 --- a/src/Presentation/Endpoints/AuthorsEndpoints.cs +++ b/src/Presentation/Endpoints/AuthorsEndpoints.cs @@ -5,6 +5,7 @@ namespace CleanMinimalApi.Presentation.Endpoints; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Entities = Application.Authors.Entities; using Queries = Application.Authors.Queries; @@ -12,29 +13,30 @@ public static class AuthorsEndpoints { public static WebApplication MapAuthorEndpoints(this WebApplication app) { - var root = app.MapGroup("/api/authors") - .WithTags("authors") + var root = app.MapGroup("/api/author") + .AddEndpointFilterFactory(ValidationFilter.ValidationFilterFactory) + .WithTags("author") + .WithDescription("Lookup and Find Authors") .WithOpenApi(); _ = root.MapGet("/", GetAuthors) .Produces>() .ProducesProblem(StatusCodes.Status500InternalServerError) .WithSummary("Lookup all Authors") - .WithDescription("\n GET /Authors"); + .WithDescription("\n GET /author"); _ = root.MapGet("/{id}", GetAuthorById) - .AddEndpointFilter>() .Produces() .ProducesValidationProblem() .ProducesProblem(StatusCodes.Status404NotFound) .ProducesProblem(StatusCodes.Status500InternalServerError) .WithSummary("Lookup an Author by their Id") - .WithDescription("\n GET /Authors/00000000-0000-0000-0000-000000000000"); + .WithDescription("\n GET /author/00000000-0000-0000-0000-000000000000"); return app; } - public static async Task GetAuthors(IMediator mediator) + public static async Task GetAuthors([FromServices] IMediator mediator) { try { @@ -46,7 +48,7 @@ public static async Task GetAuthors(IMediator mediator) } } - public static async Task GetAuthorById(Guid id, IMediator mediator) + public static async Task GetAuthorById([Validate][FromRoute] Guid id, [FromServices] IMediator mediator) { try { diff --git a/src/Presentation/Endpoints/MoviesEndpoints.cs b/src/Presentation/Endpoints/MoviesEndpoints.cs index fadf471..e085702 100644 --- a/src/Presentation/Endpoints/MoviesEndpoints.cs +++ b/src/Presentation/Endpoints/MoviesEndpoints.cs @@ -5,6 +5,7 @@ namespace CleanMinimalApi.Presentation.Endpoints; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Entities = Application.Movies.Entities; using Queries = Application.Movies.Queries; @@ -12,29 +13,30 @@ public static class MoviesEndpoints { public static WebApplication MapMovieEndpoints(this WebApplication app) { - var root = app.MapGroup("/api/movies") - .WithTags("movies") + var root = app.MapGroup("/api/movie") + .AddEndpointFilterFactory(ValidationFilter.ValidationFilterFactory) + .WithTags("movie") + .WithDescription("Lookup and Find Movies") .WithOpenApi(); _ = root.MapGet("/", GetMovies) .Produces>() .ProducesProblem(StatusCodes.Status500InternalServerError) .WithSummary("Lookup all Movies") - .WithDescription("\n GET /movies"); + .WithDescription("\n GET /movie"); - _ = root.MapGet("/{id:guid}", GetMovieById) - .AddEndpointFilter>() + _ = root.MapGet("/{id}", GetMovieById) .Produces() + .ProducesValidationProblem() .ProducesProblem(StatusCodes.Status404NotFound) .ProducesProblem(StatusCodes.Status500InternalServerError) - .ProducesValidationProblem() - .WithSummary("Lookup a Movies by its Ids") - .WithDescription("\n GET /movies/00000000-0000-0000-0000-000000000000"); + .WithSummary("Lookup a Movie by its Id") + .WithDescription("\n GET /movie/00000000-0000-0000-0000-000000000000"); return app; } - public static async Task GetMovies(IMediator mediator) + public static async Task GetMovies([FromServices] IMediator mediator) { try { @@ -46,7 +48,7 @@ public static async Task GetMovies(IMediator mediator) } } - public static async Task GetMovieById(Guid id, IMediator mediator) + public static async Task GetMovieById([Validate][FromRoute] Guid id, [FromServices] IMediator mediator) { try { diff --git a/src/Presentation/Endpoints/ReviewsEndpoints.cs b/src/Presentation/Endpoints/ReviewsEndpoints.cs index 06bb81c..15cde8a 100644 --- a/src/Presentation/Endpoints/ReviewsEndpoints.cs +++ b/src/Presentation/Endpoints/ReviewsEndpoints.cs @@ -6,7 +6,6 @@ namespace CleanMinimalApi.Presentation.Endpoints; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Commands = Application.Reviews.Commands; using Entities = Application.Reviews.Entities; @@ -16,56 +15,56 @@ public static class ReviewsEndpoints { public static WebApplication MapReviewEndpoints(this WebApplication app) { - var root = app.MapGroup("/api/reviews") - .WithTags("reviews") + var root = app.MapGroup("/api/review") + .AddEndpointFilterFactory(ValidationFilter.ValidationFilterFactory) + .WithTags("review") + .WithDescription("Lookup, Find and Manipulate Reviews") .WithOpenApi(); _ = root.MapGet("/", GetReviews) .Produces>() .ProducesProblem(StatusCodes.Status500InternalServerError) .WithSummary("Lookup all Reviews") - .WithDescription("\n GET /Reviews"); + .WithDescription("\n GET /review"); - _ = root.MapGet("/{id:guid}", GetReviewById) - .AddEndpointFilter>() + _ = root.MapGet("/{id}", GetReviewById) .Produces() + .ProducesValidationProblem() .ProducesProblem(StatusCodes.Status404NotFound) .ProducesProblem(StatusCodes.Status500InternalServerError) .ProducesValidationProblem() .WithSummary("Lookup a Review by its Ids") - .WithDescription("\n GET /Reviews/00000000-0000-0000-0000-000000000000"); + .WithDescription("\n GET /review/00000000-0000-0000-0000-000000000000"); _ = root.MapPost("/", CreateReview) - .AddEndpointFilter>() .Produces(StatusCodes.Status201Created) .ProducesProblem(StatusCodes.Status500InternalServerError) .ProducesValidationProblem() .WithSummary("Create a Review") - .WithDescription("\n POST /Reviews\n { \"authorId\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\", \"movieId\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\", \"stars\": 5 }"); + .WithDescription("\n POST /review\n { \"authorId\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\", \"movieId\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\", \"stars\": 5 }"); - _ = root.MapDelete("/{id:guid}", DeleteReview) - .AddEndpointFilter>() + _ = root.MapDelete("/{id}", DeleteReview) .Produces(StatusCodes.Status204NoContent) + .ProducesValidationProblem() .ProducesProblem(StatusCodes.Status404NotFound) .ProducesProblem(StatusCodes.Status500InternalServerError) .ProducesValidationProblem() .WithSummary("Delete a Review by its Id") - .WithDescription("\n DELETE /Reviews/00000000-0000-0000-0000-000000000000"); + .WithDescription("\n DELETE /review/00000000-0000-0000-0000-000000000000"); - _ = root.MapPut("/{id:guid}", UpdateReview) - .AddEndpointFilter>() - .AddEndpointFilter>() + _ = root.MapPut("/{id}", UpdateReview) .Produces(StatusCodes.Status204NoContent) - .ProducesProblem(StatusCodes.Status400BadRequest) + .ProducesValidationProblem() + .ProducesProblem(StatusCodes.Status404NotFound) .ProducesProblem(StatusCodes.Status500InternalServerError) .ProducesValidationProblem() .WithSummary("Update a Review") - .WithDescription("\n PUT /Reviews/00000000-0000-0000-0000-000000000000\n { \"authorId\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\", \"movieId\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\", \"stars\": 5 }"); + .WithDescription("\n PUT /review/00000000-0000-0000-0000-000000000000\n { \"authorId\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\", \"movieId\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\", \"stars\": 5 }"); return app; } - public static async Task GetReviews(IMediator mediator) + public static async Task GetReviews([FromServices] IMediator mediator) { try { @@ -77,7 +76,7 @@ public static async Task GetReviews(IMediator mediator) } } - public static async Task GetReviewById([FromRoute] Guid id, IMediator mediator) + public static async Task GetReviewById([Validate][FromRoute] Guid id, [FromServices] IMediator mediator) { try { @@ -96,21 +95,18 @@ public static async Task GetReviewById([FromRoute] Guid id, IMediator m } } - public static async Task CreateReview( - [FromBody] CreateReviewRequest request, - IMediator mediator, - HttpRequest httpRequest) + public static async Task CreateReview([Validate][FromBody] CreateReviewRequest request, [FromServices] IMediator mediator) { try { - return Results.Created( - UriHelper.GetEncodedUrl(httpRequest), - await mediator.Send(new Commands.CreateReview.CreateReviewCommand - { - AuthorId = request.AuthorId, - MovieId = request.MovieId, - Stars = request.Stars - })); + var response = await mediator.Send(new Commands.CreateReview.CreateReviewCommand + { + AuthorId = request.AuthorId, + MovieId = request.MovieId, + Stars = request.Stars + }); + + return Results.Created($"/api/review/{response.Id}", response); } catch (NotFoundException ex) { @@ -122,19 +118,16 @@ await mediator.Send(new Commands.CreateReview.CreateReviewCommand } } - public static async Task UpdateReview( - [FromRoute] Guid id, - [FromBody] UpdateReviewRequest bodyRequest, - IMediator mediator) + public static async Task UpdateReview([Validate][FromRoute] Guid id, [Validate][FromBody] UpdateReviewRequest request, [FromServices] IMediator mediator) { try { _ = await mediator.Send(new Commands.UpdateReview.UpdateReviewCommand { Id = id, - AuthorId = bodyRequest.AuthorId, - MovieId = bodyRequest.MovieId, - Stars = bodyRequest.Stars + AuthorId = request.AuthorId, + MovieId = request.MovieId, + Stars = request.Stars }); return Results.NoContent(); @@ -149,7 +142,7 @@ public static async Task UpdateReview( } } - public static async Task DeleteReview([FromRoute] Guid id, IMediator mediator) + public static async Task DeleteReview([Validate][FromRoute] Guid id, [FromServices] IMediator mediator) { try { diff --git a/src/Presentation/Extensions/WebApplicationBuilderExtensions.cs b/src/Presentation/Extensions/WebApplicationBuilderExtensions.cs index 585c250..2bb8e8c 100644 --- a/src/Presentation/Extensions/WebApplicationBuilderExtensions.cs +++ b/src/Presentation/Extensions/WebApplicationBuilderExtensions.cs @@ -86,7 +86,7 @@ public static WebApplicationBuilder ConfigureApplicationBuilder(this WebApplicat #region Validation - _ = builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); + _ = builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly(), ServiceLifetime.Singleton); #endregion Validation diff --git a/src/Presentation/Filters/ValidationAttribute.cs b/src/Presentation/Filters/ValidationAttribute.cs new file mode 100644 index 0000000..fed36fd --- /dev/null +++ b/src/Presentation/Filters/ValidationAttribute.cs @@ -0,0 +1,6 @@ +namespace CleanMinimalApi.Presentation.Filters; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] +public class ValidateAttribute : Attribute +{ +} diff --git a/src/Presentation/Filters/ValidationDescriptor.cs b/src/Presentation/Filters/ValidationDescriptor.cs new file mode 100644 index 0000000..1dac8cf --- /dev/null +++ b/src/Presentation/Filters/ValidationDescriptor.cs @@ -0,0 +1,10 @@ +namespace CleanMinimalApi.Presentation.Filters; + +using FluentValidation; + +public class ValidationDescriptor +{ + public required int ArgumentIndex { get; init; } + public required Type ArgumentType { get; init; } + public required IValidator Validator { get; init; } +} diff --git a/src/Presentation/Filters/ValidationFilter.cs b/src/Presentation/Filters/ValidationFilter.cs index 5c0b3b4..89906a3 100644 --- a/src/Presentation/Filters/ValidationFilter.cs +++ b/src/Presentation/Filters/ValidationFilter.cs @@ -1,28 +1,59 @@ namespace CleanMinimalApi.Presentation.Filters; +using System.Reflection; using FluentValidation; -public class ValidationFilter(IValidator validator) : IEndpointFilter +public static class ValidationFilter { - public async ValueTask InvokeAsync( - EndpointFilterInvocationContext context, - EndpointFilterDelegate next) + public static EndpointFilterDelegate ValidationFilterFactory(EndpointFilterFactoryContext context, EndpointFilterDelegate next) { - var x = context.Arguments.FirstOrDefault(); - var y = x.GetType(); + var validationDescriptors = GetValidators(context.MethodInfo, context.ApplicationServices); - if (context.Arguments.FirstOrDefault(x => x?.GetType() == typeof(T)) is not T argument) + if (validationDescriptors.Any()) { - return Results.BadRequest("Unable to find parameters or body for validation"); + return invocationContext => Validate(validationDescriptors, invocationContext, next); } - var validationResult = await validator.ValidateAsync(argument!); + // pass-thru + return invocationContext => next(invocationContext); + } - if (!validationResult.IsValid) + private static async ValueTask Validate(IEnumerable validationDescriptors, EndpointFilterInvocationContext invocationContext, EndpointFilterDelegate next) + { + foreach (var descriptor in validationDescriptors) { - return Results.ValidationProblem(validationResult.ToDictionary()); + var argument = invocationContext.Arguments[descriptor.ArgumentIndex]; + + if (argument is not null) + { + var validationResult = await descriptor.Validator.ValidateAsync( + new ValidationContext(argument) + ); + + if (!validationResult.IsValid) + { + return Results.ValidationProblem(validationResult.ToDictionary()); + } + } } - return await next(context); + return await next.Invoke(invocationContext); + } + + private static IEnumerable GetValidators(MethodInfo methodInfo, IServiceProvider serviceProvider) + { + foreach (var item in methodInfo.GetParameters().Select((parameter, index) => new { parameter, index })) + { + if (item.parameter.GetCustomAttribute() is not null) + { + var validatorType = typeof(IValidator<>).MakeGenericType(item.parameter.ParameterType); + var validator = serviceProvider.GetService(validatorType) as IValidator; + + if (validator is not null) + { + yield return new ValidationDescriptor { ArgumentIndex = item.index, ArgumentType = item.parameter.ParameterType, Validator = validator }; + } + } + } } } diff --git a/src/Presentation/Validators/GenericIdentityValidator.cs b/src/Presentation/Validators/GenericIdentityValidator.cs index 469a2e6..f7fad91 100644 --- a/src/Presentation/Validators/GenericIdentityValidator.cs +++ b/src/Presentation/Validators/GenericIdentityValidator.cs @@ -6,6 +6,9 @@ public class GenericIdentityValidator : AbstractValidator { public GenericIdentityValidator() { - _ = this.RuleFor(r => r).NotEqual(Guid.Empty).WithMessage("A valid Id was not supplied."); + _ = this.RuleFor(r => r) + .NotNull() + .NotEqual(Guid.Empty) + .WithMessage("The Id supplied in the request is not valid."); } } diff --git a/tests/Application.Tests.Unit/Authors/Queries/GetAuthorById/GetAuthorByIdHandlerTests.cs b/tests/Application.Tests.Unit/Authors/Queries/GetAuthorById/GetAuthorByIdHandlerTests.cs index 39d8058..d280385 100644 --- a/tests/Application.Tests.Unit/Authors/Queries/GetAuthorById/GetAuthorByIdHandlerTests.cs +++ b/tests/Application.Tests.Unit/Authors/Queries/GetAuthorById/GetAuthorByIdHandlerTests.cs @@ -25,12 +25,7 @@ public async Task Handle_ShouldPassThrough_Query() var handler = new GetAuthorByIdHandler(context); var token = new CancellationTokenSource().Token; - _ = context.GetAuthorById(Arg.Any(), token).Returns(new Author - { - Id = Guid.Empty, - FirstName = "FirstName", - LastName = "LastName" - }); + _ = context.GetAuthorById(Arg.Any(), token).Returns(new Author(Guid.Empty, "FirstName", "LastName")); // Act var result = await handler.Handle(query, token); @@ -44,6 +39,7 @@ public async Task Handle_ShouldPassThrough_Query() result.Id.ShouldBe(Guid.Empty); result.FirstName.ShouldBe("FirstName"); result.LastName.ShouldBe("LastName"); + result.Reviews.ShouldBeNull(); } diff --git a/tests/Application.Tests.Unit/Authors/Queries/GetAuthors/GetAuthorsHandlerTests.cs b/tests/Application.Tests.Unit/Authors/Queries/GetAuthors/GetAuthorsHandlerTests.cs index a12ba0a..2c8cf10 100644 --- a/tests/Application.Tests.Unit/Authors/Queries/GetAuthors/GetAuthorsHandlerTests.cs +++ b/tests/Application.Tests.Unit/Authors/Queries/GetAuthors/GetAuthorsHandlerTests.cs @@ -21,14 +21,7 @@ public async Task Handle_ShouldPassThrough_Query() var handler = new GetAuthorsHandler(context); var token = new CancellationTokenSource().Token; - _ = context.GetAuthors(token).Returns([ - new Author - { - Id = Guid.Empty, - FirstName = "FirstName", - LastName = "LastName" - } - ]); + _ = context.GetAuthors(token).Returns([new Author(Guid.Empty, "FirstName", "LastName")]); // Act var result = await handler.Handle(query, token); diff --git a/tests/Application.Tests.Unit/Common/Exceptions/NotFoundExceptionTests.cs b/tests/Application.Tests.Unit/Common/Exceptions/NotFoundExceptionTests.cs index 82ca458..8e9b1a6 100644 --- a/tests/Application.Tests.Unit/Common/Exceptions/NotFoundExceptionTests.cs +++ b/tests/Application.Tests.Unit/Common/Exceptions/NotFoundExceptionTests.cs @@ -13,10 +13,7 @@ public void ThrowIfNull_ShouldNotThrow_NotFoundException() { // Arrange var entityType = EntityType.Author; - var argument = new Author - { - Id = Guid.NewGuid() - }; + var argument = new Author(Guid.NewGuid(), "FirstName", "LastName"); // Act var result = Should.NotThrow(() => diff --git a/tests/Application.Tests.Unit/Movies/Queries/GetMovieById/GetReviewByIdHandlerTests.cs b/tests/Application.Tests.Unit/Movies/Queries/GetMovieById/GetReviewByIdHandlerTests.cs index a9f6dc2..7262264 100644 --- a/tests/Application.Tests.Unit/Movies/Queries/GetMovieById/GetReviewByIdHandlerTests.cs +++ b/tests/Application.Tests.Unit/Movies/Queries/GetMovieById/GetReviewByIdHandlerTests.cs @@ -25,11 +25,7 @@ public async Task Handle_ShouldPassThrough_Query() var handler = new GetMovieByIdHandler(context); var token = new CancellationTokenSource().Token; - _ = context.GetMovieById(Arg.Any(), token).Returns(new Movie - { - Id = Guid.Empty, - Title = "Title" - }); + _ = context.GetMovieById(Arg.Any(), token).Returns(new Movie(Guid.Empty, "Title")); // Act var result = await handler.Handle(query, token); @@ -42,6 +38,7 @@ public async Task Handle_ShouldPassThrough_Query() result.Id.ShouldBe(Guid.Empty); result.Title.ShouldBe("Title"); + result.Reviews.ShouldBeNull(); } [Fact] diff --git a/tests/Application.Tests.Unit/Movies/Queries/GetMovies/GetMoviesHandlerTests.cs b/tests/Application.Tests.Unit/Movies/Queries/GetMovies/GetMoviesHandlerTests.cs index 6614471..a3da156 100644 --- a/tests/Application.Tests.Unit/Movies/Queries/GetMovies/GetMoviesHandlerTests.cs +++ b/tests/Application.Tests.Unit/Movies/Queries/GetMovies/GetMoviesHandlerTests.cs @@ -21,14 +21,7 @@ public async Task Handle_ShouldPassThrough_Query() var handler = new GetMoviesHandler(context); var token = new CancellationTokenSource().Token; - _ = context.GetMovies(token).Returns( - [ - new Movie - { - Id = Guid.Empty, - Title = "Title" - } - ]); + _ = context.GetMovies(token).Returns([new Movie(Guid.Empty, "Title")]); // Act var result = await handler.Handle(query, token); diff --git a/tests/Application.Tests.Unit/Reviews/Queries/GetReviewById/GetReviewByIdHandlerTests.cs b/tests/Application.Tests.Unit/Reviews/Queries/GetReviewById/GetReviewByIdHandlerTests.cs index e11d533..abafef0 100644 --- a/tests/Application.Tests.Unit/Reviews/Queries/GetReviewById/GetReviewByIdHandlerTests.cs +++ b/tests/Application.Tests.Unit/Reviews/Queries/GetReviewById/GetReviewByIdHandlerTests.cs @@ -25,11 +25,7 @@ public async Task Handle_ShouldPassThrough_Query() var handler = new GetReviewByIdHandler(context); var token = new CancellationTokenSource().Token; - _ = context.GetReviewById(Arg.Any(), token).Returns(new Review - { - Id = Guid.Empty, - Stars = 5 - }); + _ = context.GetReviewById(Arg.Any(), token).Returns(new Review(Guid.Empty, 5)); // Act var result = await handler.Handle(query, token); diff --git a/tests/Application.Tests.Unit/Reviews/Queries/GetReviews/GetReviewsHandlerTests.cs b/tests/Application.Tests.Unit/Reviews/Queries/GetReviews/GetReviewsHandlerTests.cs index 99cec68..8bf97d5 100644 --- a/tests/Application.Tests.Unit/Reviews/Queries/GetReviews/GetReviewsHandlerTests.cs +++ b/tests/Application.Tests.Unit/Reviews/Queries/GetReviews/GetReviewsHandlerTests.cs @@ -21,13 +21,7 @@ public async Task Handle_ShouldPassThrough_Query() var handler = new GetReviewsHandler(context); var token = new CancellationTokenSource().Token; - _ = context.GetReviews(token).Returns([ - new Review - { - Id = Guid.Empty, - Stars = 5 - } - ]); + _ = context.GetReviews(token).Returns([new Review(Guid.Empty, 5)]); // Act var result = await handler.Handle(query, token); diff --git a/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs b/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs index b9f1ab3..a3d0b97 100644 --- a/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs +++ b/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs @@ -197,8 +197,6 @@ public async void CreateReview_ShouldReturn_NewReviews() result.ReviewAuthor.Id.ShouldBe(review.AuthorId); result.ReviewedMovie.Id.ShouldBe(review.MovieId); result.Stars.ShouldBe(review.Stars); - result.DateCreated.ShouldBe(fixture.TimeProvider.GetUtcNow().UtcDateTime); - result.DateModified.ShouldBe(fixture.TimeProvider.GetUtcNow().UtcDateTime); // Cleanup _ = await fixture.Repository.DeleteReview(result.Id, token); diff --git a/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/MovieReviewsCollectionFixture.cs b/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/MovieReviewsCollectionFixture.cs index daf1606..6f44bea 100644 --- a/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/MovieReviewsCollectionFixture.cs +++ b/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/MovieReviewsCollectionFixture.cs @@ -37,13 +37,12 @@ public MovieReviewsDataFixture() this.Mapper = new MapperConfiguration(cfg => cfg - .AddProfiles(new List() - { + .AddProfiles( + [ new AuthorMappingProfile(), - new EntitiyMappingProfile(), new MovieMappingProfile(), new ReviewMappingProfile() - })) + ])) .CreateMapper(); this.Repository = new EntityFrameworkMovieReviewsRepository(this.Context, this.TimeProvider, this.Mapper); diff --git a/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/MovieReviewsConfigurationTests.cs b/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/MovieReviewsConfigurationTests.cs index 885c78f..c790cf5 100644 --- a/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/MovieReviewsConfigurationTests.cs +++ b/tests/Infrastructure.Tests.Integration/Databases/MovieReviews/MovieReviewsConfigurationTests.cs @@ -16,9 +16,16 @@ public async void Database_ShouldBe_Configured() var token = new CancellationTokenSource().Token; // Act - var author = await context.Authors.Include(a => a.Reviews).FirstOrDefaultAsync(a => a.FirstName == "One", token); - var movie = await context.Movies.Include(m => m.Reviews).FirstOrDefaultAsync(m => m.Title == "One", token); - var review = await context.Reviews.Include(r => r.ReviewAuthor).Include(r => r.ReviewedMovie).FirstOrDefaultAsync(m => m.Stars == 5, token); + var author = await context.Authors + .Include(a => a.Reviews) + .FirstOrDefaultAsync(a => a.FirstName == "One", token); + var movie = await context.Movies + .Include(m => m.Reviews) + .FirstOrDefaultAsync(m => m.Title == "One", token); + var review = await context.Reviews + .Include(r => r.ReviewAuthor) + .Include(r => r.ReviewedMovie) + .FirstOrDefaultAsync(m => m.Stars == 5, token); // Assert context.Database.IsInMemory().ShouldBeTrue(); diff --git a/tests/Presentation.Tests.Integration/Endpoints/AuthorEndpointTests.cs b/tests/Presentation.Tests.Integration/Endpoints/AuthorEndpointTests.cs index 5a1e867..d572671 100644 --- a/tests/Presentation.Tests.Integration/Endpoints/AuthorEndpointTests.cs +++ b/tests/Presentation.Tests.Integration/Endpoints/AuthorEndpointTests.cs @@ -4,6 +4,7 @@ namespace CleanMinimalApi.Presentation.Tests.Integration.Endpoints; using System.Net; using System.Threading.Tasks; using Extensions; +using Microsoft.AspNetCore.Mvc; using Shouldly; using Xunit; using Entities = Application.Authors.Entities; @@ -24,7 +25,7 @@ public async Task GetAuthors_ShouldReturn_Ok() using var client = this.application.CreateClient(); // Act - using var response = await client.GetAsync("/api/authors"); + using var response = await client.GetAsync("/api/author"); var result = (await response.Content.ReadAsStringAsync()) .Deserialize>(); @@ -62,12 +63,12 @@ public async Task GetAuthorById_ShouldReturn_Ok() { // Arrange using var client = this.application.CreateClient(); - using var authorResponse = await client.GetAsync("/api/authors"); + using var authorResponse = await client.GetAsync("/api/author"); var authorResult = (await authorResponse.Content.ReadAsStringAsync()) .Deserialize>()[0]; // Act - using var response = await client.GetAsync($"/api/authors/{authorResult.Id}"); + using var response = await client.GetAsync($"/api/author/{authorResult.Id}"); var result = (await response.Content.ReadAsStringAsync()).Deserialize(); // Assert @@ -93,6 +94,30 @@ public async Task GetAuthorById_ShouldReturn_Ok() } } + [Theory] + [InlineData("00000000-0000-0000-0000-000000000000")] + [InlineData("1")] + [InlineData("fake")] + public async Task GetAuthorById_ShouldReturn_ValidationProblem(string input) + { + // Arrange + using var client = this.application.CreateClient(); + + // Act + using var response = await client.GetAsync($"/api/author/{input}"); + var result = (await response.Content.ReadAsStringAsync()).Deserialize(); + + // Assert + response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); + + _ = result.ShouldNotBeNull(); + + result.Errors.ShouldNotBeEmpty(); + + result.Errors.ShouldContainKey(""); + result.Errors[""].ShouldBe(["The Id supplied in the request is not valid."]); + } + public void Dispose() { this.Dispose(true); @@ -110,15 +135,4 @@ protected virtual void Dispose(bool disposing) } } } - - -} - - -public class ErrorModel -{ - public string Type { get; set; } - public string Title { get; set; } - public int Status { get; set; } - public Dictionary> Errors { get; set; } } diff --git a/tests/Presentation.Tests.Integration/Endpoints/MovieEndpointTests.cs b/tests/Presentation.Tests.Integration/Endpoints/MovieEndpointTests.cs index fb390e3..a358310 100644 --- a/tests/Presentation.Tests.Integration/Endpoints/MovieEndpointTests.cs +++ b/tests/Presentation.Tests.Integration/Endpoints/MovieEndpointTests.cs @@ -4,6 +4,7 @@ namespace CleanMinimalApi.Presentation.Tests.Integration.Endpoints; using System.Net; using System.Threading.Tasks; using Extensions; +using Microsoft.AspNetCore.Mvc; using Shouldly; using Xunit; using Entities = Application.Movies.Entities; @@ -24,7 +25,7 @@ public async Task GetMovies_ShouldReturn_Ok() using var client = this.application.CreateClient(); // Act - using var response = await client.GetAsync("/api/movies"); + using var response = await client.GetAsync("/api/movie"); var result = (await response.Content.ReadAsStringAsync()) .Deserialize>(); @@ -63,12 +64,12 @@ public async Task GetMovieById_ShouldReturn_Ok() { // Arrange using var client = this.application.CreateClient(); - using var movieResponse = await client.GetAsync("/api/movies"); + using var movieResponse = await client.GetAsync("/api/movie"); var movieResult = (await movieResponse.Content.ReadAsStringAsync()) .Deserialize>()[0]; // Act - using var response = await client.GetAsync($"/api/movies/{movieResult.Id}"); + using var response = await client.GetAsync($"/api/movie/{movieResult.Id}"); var result = (await response.Content.ReadAsStringAsync()).Deserialize(); // Assert @@ -95,6 +96,30 @@ public async Task GetMovieById_ShouldReturn_Ok() } } + [Theory] + [InlineData("00000000-0000-0000-0000-000000000000")] + [InlineData("1")] + [InlineData("fake")] + public async Task GetMovieId_ShouldReturn_ValidationProblem(string input) + { + // Arrange + using var client = this.application.CreateClient(); + + // Act + using var response = await client.GetAsync($"/api/movie/{input}"); + var result = (await response.Content.ReadAsStringAsync()).Deserialize(); + + // Assert + response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); + + _ = result.ShouldNotBeNull(); + + result.Errors.ShouldNotBeEmpty(); + + result.Errors.ShouldContainKey(""); + result.Errors[""].ShouldBe(["The Id supplied in the request is not valid."]); + } + public void Dispose() { this.Dispose(true); diff --git a/tests/Presentation.Tests.Integration/Endpoints/ReviewEndpointTests.cs b/tests/Presentation.Tests.Integration/Endpoints/ReviewEndpointTests.cs index 0cbb6c3..0276504 100644 --- a/tests/Presentation.Tests.Integration/Endpoints/ReviewEndpointTests.cs +++ b/tests/Presentation.Tests.Integration/Endpoints/ReviewEndpointTests.cs @@ -8,6 +8,7 @@ namespace CleanMinimalApi.Presentation.Tests.Integration.Endpoints; using Application.Authors.Entities; using Application.Movies.Entities; using Extensions; +using Microsoft.AspNetCore.Mvc; using Shouldly; using Xunit; using Entities = Application.Reviews.Entities; @@ -27,15 +28,15 @@ public async Task CreateReview_ShouldReturn_Created() // Arrange using var client = this.application.CreateClient(); - using var authorResponse = await client.GetAsync("/api/authors"); + using var authorResponse = await client.GetAsync("/api/author"); var authorResult = (await authorResponse.Content.ReadAsStringAsync()).Deserialize>()[0]; - using var movieResponse = await client.GetAsync("/api/movies"); + using var movieResponse = await client.GetAsync("/api/movie"); var movieResult = (await movieResponse.Content.ReadAsStringAsync()).Deserialize>()[0]; var json = (new { Stars = 5, AuthorId = authorResult.Id, MovieId = movieResult.Id }).Serialize(); var content = new StringContent(json, Encoding.UTF8, "application/json"); // Act - using var response = await client.PostAsync("/api/reviews", content); + using var response = await client.PostAsync("/api/review", content); var result = (await response.Content.ReadAsStringAsync()).Deserialize(); // Assert @@ -62,17 +63,17 @@ public async Task DeleteReview_ShouldReturn_NoContent() { // Arrange using var client = this.application.CreateClient(); - using var reviewResponse = await client.GetAsync("/api/reviews"); + using var reviewResponse = await client.GetAsync("/api/review"); var reviewResult = (await reviewResponse.Content.ReadAsStringAsync()) .Deserialize>()[0]; // Act - using var response = await client.DeleteAsync($"/api/reviews/{reviewResult.Id}"); + using var response = await client.DeleteAsync($"/api/review/{reviewResult.Id}"); // Assert response.StatusCode.ShouldBe(HttpStatusCode.NoContent); - (await client.GetAsync($"/api/reviews/{reviewResult.Id}")).StatusCode.ShouldBe(HttpStatusCode.NotFound); + (await client.GetAsync($"/api/review/{reviewResult.Id}")).StatusCode.ShouldBe(HttpStatusCode.NotFound); } [Fact] @@ -82,7 +83,7 @@ public async Task GetReviews_ShouldReturn_Ok() using var client = this.application.CreateClient(); // Act - using var response = await client.GetAsync("/api/reviews"); + using var response = await client.GetAsync("/api/review"); var result = (await response.Content.ReadAsStringAsync()) .Deserialize>(); @@ -120,12 +121,12 @@ public async Task GetReviewById_ShouldReturn_Ok() { // Arrange using var client = this.application.CreateClient(); - using var reviewResponse = await client.GetAsync("/api/reviews"); + using var reviewResponse = await client.GetAsync("/api/review"); var reviewResult = (await reviewResponse.Content.ReadAsStringAsync()) .Deserialize>()[0]; // Act - using var response = await client.GetAsync($"/api/reviews/{reviewResult.Id}"); + using var response = await client.GetAsync($"/api/review/{reviewResult.Id}"); var result = (await response.Content.ReadAsStringAsync()).Deserialize(); // Assert @@ -151,21 +152,45 @@ public async Task GetReviewById_ShouldReturn_Ok() _ = result.ReviewedMovie.Title.ShouldBeOfType(); } + [Theory] + [InlineData("00000000-0000-0000-0000-000000000000")] + [InlineData("1")] + [InlineData("fake")] + public async Task GetReviewId_ShouldReturn_ValidationProblem(string input) + { + // Arrange + using var client = this.application.CreateClient(); + + // Act + using var response = await client.GetAsync($"/api/review/{input}"); + var result = (await response.Content.ReadAsStringAsync()).Deserialize(); + + // Assert + response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); + + _ = result.ShouldNotBeNull(); + + result.Errors.ShouldNotBeEmpty(); + + result.Errors.ShouldContainKey(""); + result.Errors[""].ShouldBe(["The Id supplied in the request is not valid."]); + } + [Fact] public async Task UpdateReview_ShouldReturn_NoContent() { // Arrange using var client = this.application.CreateClient(); - using var authorResponse = await client.GetAsync("/api/authors"); + using var authorResponse = await client.GetAsync("/api/author"); var authorResult = (await authorResponse.Content.ReadAsStringAsync()) .Deserialize>()[0]; - using var movieResponse = await client.GetAsync("/api/movies"); + using var movieResponse = await client.GetAsync("/api/movie"); var movieResult = (await movieResponse.Content.ReadAsStringAsync()) .Deserialize>()[0]; - using var reviewResponse = await client.GetAsync("/api/reviews"); + using var reviewResponse = await client.GetAsync("/api/review"); var reviewResult = (await reviewResponse.Content.ReadAsStringAsync()) .Deserialize>()[0]; @@ -173,12 +198,12 @@ public async Task UpdateReview_ShouldReturn_NoContent() var content = new StringContent(json, Encoding.UTF8, "application/json"); // Act - using var response = await client.PutAsync($"/api/reviews/{reviewResult.Id}", content); + using var response = await client.PutAsync($"/api/review/{reviewResult.Id}", content); // Assert response.StatusCode.ShouldBe(HttpStatusCode.NoContent); - using var validateResponse = await client.GetAsync($"/api/reviews/{reviewResult.Id}"); + using var validateResponse = await client.GetAsync($"/api/review/{reviewResult.Id}"); var validateResult = (await validateResponse.Content.ReadAsStringAsync()).Deserialize(); _ = validateResult.ShouldNotBeNull(); diff --git a/tests/Presentation.Tests.Unit/Endpoints/AuthorEndpointTests.cs b/tests/Presentation.Tests.Unit/Endpoints/AuthorEndpointTests.cs index 5f2737a..fae1875 100644 --- a/tests/Presentation.Tests.Unit/Endpoints/AuthorEndpointTests.cs +++ b/tests/Presentation.Tests.Unit/Endpoints/AuthorEndpointTests.cs @@ -2,6 +2,7 @@ namespace CleanMinimalApi.Presentation.Tests.Unit.Endpoints; using System.Threading.Tasks; using CleanMinimalApi.Application.Common.Exceptions; +using CleanMinimalApi.Application.Reviews.Entities; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; @@ -25,12 +26,7 @@ public async Task GetAuthors_ShouldReturn_Ok() .Send(Arg.Any()) .ReturnsForAnyArgs( [ - new() - { - Id = Guid.Empty, - FirstName = "Lorem", - LastName = "Ipsum" - } + new Entities.Author(Guid.Empty, "Lorem", "Ipsum") ]); // Act @@ -49,6 +45,8 @@ public async Task GetAuthors_ShouldReturn_Ok() value[0].FirstName.ShouldBe("Lorem"); _ = value[0].LastName.ShouldBeOfType(); value[0].LastName.ShouldBe("Ipsum"); + _ = value[0].Reviews.ShouldBeAssignableTo>(); + value[0].Reviews.ShouldBeNull(); } [Fact] @@ -81,12 +79,9 @@ public async Task GetAuthorById_ShouldReturn_Ok() // Arrange var mediator = Substitute.For(); - _ = mediator.Send(Arg.Any()).ReturnsForAnyArgs(new Entities.Author - { - Id = Guid.Empty, - FirstName = "Lorem", - LastName = "Ipsum" - }); + _ = mediator + .Send(Arg.Any()) + .ReturnsForAnyArgs(new Entities.Author(Guid.Empty, "Lorem", "Ipsum")); // Act var response = await AuthorsEndpoints.GetAuthorById(Guid.Empty, mediator); @@ -104,6 +99,8 @@ public async Task GetAuthorById_ShouldReturn_Ok() value.FirstName.ShouldBe("Lorem"); _ = value.LastName.ShouldBeOfType(); value.LastName.ShouldBe("Ipsum"); + _ = value.Reviews.ShouldBeAssignableTo>(); + value.Reviews.ShouldBeNull(); } [Fact] diff --git a/tests/Presentation.Tests.Unit/Endpoints/MovieEndpointTests.cs b/tests/Presentation.Tests.Unit/Endpoints/MovieEndpointTests.cs index 5c452e6..db1dde9 100644 --- a/tests/Presentation.Tests.Unit/Endpoints/MovieEndpointTests.cs +++ b/tests/Presentation.Tests.Unit/Endpoints/MovieEndpointTests.cs @@ -2,6 +2,7 @@ namespace CleanMinimalApi.Presentation.Tests.Unit.Endpoints; using System.Threading.Tasks; using CleanMinimalApi.Application.Common.Exceptions; +using CleanMinimalApi.Application.Reviews.Entities; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; @@ -25,11 +26,7 @@ public async Task GetMovies_ShouldReturn_Ok() .Send(Arg.Any()) .ReturnsForAnyArgs( [ - new() - { - Id = Guid.Empty, - Title = "Lorem Ipsum" - } + new Entities.Movie(Guid.Empty, "Lorem Ipsum") ]); // Act @@ -46,6 +43,8 @@ public async Task GetMovies_ShouldReturn_Ok() value[0].Id.ShouldBe(Guid.Empty); _ = value[0].Title.ShouldBeOfType(); value[0].Title.ShouldBe("Lorem Ipsum"); + _ = value[0].Reviews.ShouldBeAssignableTo>(); + value[0].Reviews.ShouldBeNull(); } [Fact] @@ -80,11 +79,7 @@ public async Task GetMovieById_ShouldReturn_Ok() _ = mediator .Send(Arg.Any()) - .ReturnsForAnyArgs(new Entities.Movie - { - Id = Guid.Empty, - Title = "Lorem Ipsum", - }); + .ReturnsForAnyArgs(new Entities.Movie(Guid.Empty, "Lorem Ipsum")); // Act var response = await MoviesEndpoints.GetMovieById(Guid.Empty, mediator); @@ -100,6 +95,8 @@ public async Task GetMovieById_ShouldReturn_Ok() value.Id.ShouldBe(Guid.Empty); _ = value.Title.ShouldBeOfType(); value.Title.ShouldBe("Lorem Ipsum"); + _ = value.Reviews.ShouldBeAssignableTo>(); + value.Reviews.ShouldBeNull(); } [Fact] diff --git a/tests/Presentation.Tests.Unit/Endpoints/ReviewEndpointTests.cs b/tests/Presentation.Tests.Unit/Endpoints/ReviewEndpointTests.cs index 906b62c..b6693f7 100644 --- a/tests/Presentation.Tests.Unit/Endpoints/ReviewEndpointTests.cs +++ b/tests/Presentation.Tests.Unit/Endpoints/ReviewEndpointTests.cs @@ -28,22 +28,12 @@ public async Task GetReviews_ShouldReturn_Ok() .Send(Arg.Any()) .ReturnsForAnyArgs( [ - new() - { - Id = Guid.Empty, - Stars = 5, - ReviewAuthor = new ReviewAuthor - { - Id = Guid.Empty, - FirstName = "Lorem", - LastName = "Ipsum" - }, - ReviewedMovie = new ReviewMovie - { - Id = Guid.Empty, - Title = "Lorem Ipsum" - } - } + new Entities.Review( + Guid.Empty, + 5, + new ReviewedMovie(Guid.Empty, "Lorem Ipsum"), + new ReviewAuthor(Guid.Empty, "Lorem", "Ipsum") + ) ]); // Act @@ -106,22 +96,12 @@ public async Task GetReviewById_ShouldReturn_Ok() _ = mediator .Send(Arg.Any()) - .ReturnsForAnyArgs(new Entities.Review - { - Id = Guid.Empty, - Stars = 5, - ReviewAuthor = new ReviewAuthor - { - Id = Guid.Empty, - FirstName = "Lorem", - LastName = "Ipsum" - }, - ReviewedMovie = new ReviewMovie - { - Id = Guid.Empty, - Title = "Lorem Ipsum" - } - }); + .ReturnsForAnyArgs(new Entities.Review( + Guid.Empty, + 5, + new ReviewedMovie(Guid.Empty, "Lorem Ipsum"), + new ReviewAuthor(Guid.Empty, "Lorem", "Ipsum") + )); // Act var response = await ReviewsEndpoints.GetReviewById(Guid.Empty, mediator); @@ -199,7 +179,7 @@ public async Task GetReviewById_ShouldReturn_Problem() public async Task CreateReview_ShouldReturn_Created() { // Arrange - var httpRequest = Substitute.For(); + //var httpRequest = Substitute.For(); var mediator = Substitute.For(); var request = new Requests.CreateReviewRequest { @@ -210,25 +190,15 @@ public async Task CreateReview_ShouldReturn_Created() _ = mediator .Send(Arg.Any()) - .ReturnsForAnyArgs(new Entities.Review - { - Id = Guid.Empty, - Stars = 5, - ReviewAuthor = new ReviewAuthor - { - Id = Guid.Empty, - FirstName = "Lorem", - LastName = "Ipsum" - }, - ReviewedMovie = new ReviewMovie - { - Id = Guid.Empty, - Title = "Lorem Ipsum" - } - }); + .ReturnsForAnyArgs(new Entities.Review( + Guid.Empty, + 5, + new ReviewedMovie(Guid.Empty, "Lorem Ipsum"), + new ReviewAuthor(Guid.Empty, "Lorem", "Ipsum") + )); // Act - var response = await ReviewsEndpoints.CreateReview(request, mediator, httpRequest); + var response = await ReviewsEndpoints.CreateReview(request, mediator); // Assert var result = response.ShouldBeOfType>(); @@ -259,7 +229,7 @@ public async Task CreateReview_ShouldReturn_Created() public async Task CreateReview_ShouldReturn_NotFound() { // Arrange - var httpRequest = Substitute.For(); + //var httpRequest = Substitute.For(); var mediator = Substitute.For(); var request = new Requests.CreateReviewRequest { @@ -273,7 +243,7 @@ public async Task CreateReview_ShouldReturn_NotFound() .Throws(new NotFoundException("Expected Exception")); // Act - var response = await ReviewsEndpoints.CreateReview(request, mediator, httpRequest); + var response = await ReviewsEndpoints.CreateReview(request, mediator); // Assert var result = response.ShouldBeOfType>(); @@ -300,7 +270,7 @@ public async Task CreateReview_ShouldReturn_Problem() .Throws(new ArgumentException("Expected Exception")); // Act - var response = await ReviewsEndpoints.CreateReview(request, mediator, httpRequest); + var response = await ReviewsEndpoints.CreateReview(request, mediator); // Assert var result = response.ShouldBeOfType(); @@ -444,7 +414,7 @@ public async Task DeleteReview_ShouldReturn_Problem() .Throws(new ArgumentException("Expected Exception")); // Act - var response = await ReviewsEndpoints.DeleteReview(Guid.NewGuid(), mediator); + var response = await ReviewsEndpoints.DeleteReview(Guid.Empty, mediator); // Assert var result = response.ShouldBeOfType(); diff --git a/tests/Presentation.Tests.Unit/Filters/ValidationFilterTests.cs b/tests/Presentation.Tests.Unit/Filters/ValidationFilterTests.cs deleted file mode 100644 index 0586210..0000000 --- a/tests/Presentation.Tests.Unit/Filters/ValidationFilterTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -namespace CleanMinimalApi.Presentation.Tests.Unit.Filters; - -using System.Threading.Tasks; -using CleanMinimalApi.Presentation.Filters; -using CleanMinimalApi.Presentation.Validators; -using FluentValidation; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.HttpResults; -using Shouldly; -using Xunit; - -public class ValidationFilterTests -{ - private readonly IValidator validator = new GenericIdentityValidator(); - - [Fact] - public async Task InvokeAsync_ValidValue_ReturnsNext() - { - // Arrange - var value = Guid.NewGuid(); - var filter = new ValidationFilter(this.validator); - var context = CreateFilterContext(value); - - // Act - var response = await filter.InvokeAsync(context, NextDelegate); - - // Assert - response.ShouldBeSameAs(NextResult); - } - - [Fact] - public async Task InvokeAsync_ValidValue_BadRequest() - { - // Arrange - var value = "Guid.NewGuid()"; - var filter = new ValidationFilter(this.validator); - var context = CreateFilterContext(value); - - // Act - var response = await filter.InvokeAsync(context, NextDelegate); - - // Assert - var result = response.ShouldBeOfType>(); - - result.StatusCode.ShouldBe(400); - result.Value.ShouldBe("Unable to find parameters or body for validation"); - } - - [Fact] - public async Task InvokeAsync_ValidValue_ValidationProblem() - { - // Arrange - var value = Guid.Empty; - var filter = new ValidationFilter(this.validator); - var context = CreateFilterContext(value); - - // Act - var response = await filter.InvokeAsync(context, NextDelegate); - - // Assert - var result = response.ShouldBeOfType(); - - result.StatusCode.ShouldBe(400); - _ = result.ProblemDetails.ShouldNotBeNull(); - result.ProblemDetails.Title.ShouldBe("One or more validation errors occurred."); - } - - #region Helper Methods - - private static DefaultEndpointFilterInvocationContext CreateFilterContext(object argument) - { - var httpContext = new DefaultHttpContext(); - var filterContext = new DefaultEndpointFilterInvocationContext(httpContext, argument); - - return filterContext; - } - - private static ValueTask NextDelegate(EndpointFilterInvocationContext context) - { - return ValueTask.FromResult(NextResult); - } - - private static readonly object NextResult = new(); - - #endregion Helper Methods -}