From 6e7dbff7b112b34f318b10488e12e511ae3bfc7f Mon Sep 17 00:00:00 2001 From: Jonathan Potts Date: Thu, 1 Feb 2024 18:42:51 -0600 Subject: [PATCH] Add FluentValidation to WebApi --- README.md | 1 + src/WebApi/Apis/CuisinesApi.cs | 30 ++-------- src/WebApi/Apis/RecipesApi.cs | 56 ++----------------- .../CuisineEntityTypeConfiguration.cs | 3 + .../RecipeEntityTypeConfiguration.cs | 32 ++++++++++- .../ApplicationDbContextModelSnapshot.cs | 12 ++-- src/WebApi/Models/Cuisine.cs | 5 +- src/WebApi/Models/CuisineCreateOrUpdateDto.cs | 5 +- src/WebApi/Models/CuisineDto.cs | 5 +- src/WebApi/Models/ImageData.cs | 5 +- src/WebApi/Models/MarkdownData.cs | 6 +- src/WebApi/Models/Recipe.cs | 9 +-- src/WebApi/Models/RecipeCreateOrUpdateDto.cs | 7 +-- src/WebApi/Models/RecipeDto.cs | 5 +- src/WebApi/Models/RecipeWithCuisineDto.cs | 5 +- src/WebApi/Program.cs | 6 ++ src/WebApi/README.md | 1 + .../CuisineCreateOrUpdateDtoValidator.cs | 12 ++++ src/WebApi/Validation/CuisineDtoValidator.cs | 13 +++++ src/WebApi/Validation/CuisineValidator.cs | 12 ++++ src/WebApi/Validation/ImageDataValidator.cs | 12 ++++ .../Validation/MarkdownDataValidator.cs | 13 +++++ .../RecipeCreateOrUpdateDtoValidator.cs | 15 +++++ src/WebApi/Validation/RecipeDtoValidator.cs | 14 +++++ src/WebApi/Validation/RecipeValidator.cs | 16 ++++++ .../RecipeWithCuisineDtoValidator.cs | 12 ++++ src/WebApi/WebApi.csproj | 4 ++ .../WebApi.Tests/Apis/RecipesApiUnitTests.cs | 20 ------- 28 files changed, 189 insertions(+), 147 deletions(-) create mode 100644 src/WebApi/Validation/CuisineCreateOrUpdateDtoValidator.cs create mode 100644 src/WebApi/Validation/CuisineDtoValidator.cs create mode 100644 src/WebApi/Validation/CuisineValidator.cs create mode 100644 src/WebApi/Validation/ImageDataValidator.cs create mode 100644 src/WebApi/Validation/MarkdownDataValidator.cs create mode 100644 src/WebApi/Validation/RecipeCreateOrUpdateDtoValidator.cs create mode 100644 src/WebApi/Validation/RecipeDtoValidator.cs create mode 100644 src/WebApi/Validation/RecipeValidator.cs create mode 100644 src/WebApi/Validation/RecipeWithCuisineDtoValidator.cs diff --git a/README.md b/README.md index 97c8c11..de6f49a 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ RecipeCatalog is a [.NET](https://dotnet.microsoft.com/) 8 project showcasing a - [Entity Framework Core](https://learn.microsoft.com/ef/core/) - [Migrations](https://learn.microsoft.com/ef/core/managing-schemas/migrations/) - [SQLite](https://www.sqlite.org/) +- [FluentValidation](https://github.com/FluentValidation/FluentValidation) - Markdown ([Markdig](https://github.com/xoofx/markdig)) - Snowflake IDs ([IdGen](https://github.com/RobThree/IdGen)) diff --git a/src/WebApi/Apis/CuisinesApi.cs b/src/WebApi/Apis/CuisinesApi.cs index efd0c49..053fa4f 100644 --- a/src/WebApi/Apis/CuisinesApi.cs +++ b/src/WebApi/Apis/CuisinesApi.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; +using SharpGrip.FluentValidation.AutoValidation.Endpoints.Extensions; namespace JonathanPotts.RecipeCatalog.WebApi.Apis; @@ -13,6 +14,7 @@ public static class CuisinesApi public static IEndpointRouteBuilder MapCuisinesApi(this IEndpointRouteBuilder builder) { var group = builder.MapGroup("/api/v1/cuisines") + .AddFluentValidationAutoValidation() .WithTags("Cuisines"); group.MapGet("/", GetListAsync); @@ -61,23 +63,11 @@ public static async Task, NotFound>> GetAsync( } [Authorize] - public static async Task, ValidationProblem, ForbidHttpResult>> PostAsync( + public static async Task, ForbidHttpResult>> PostAsync( [AsParameters] Services services, ClaimsPrincipal user, CuisineDto dto) { - Dictionary errors = []; - - if (string.IsNullOrWhiteSpace(dto.Name)) - { - errors.Add(nameof(dto.Name), ["Value is required."]); - } - - if (errors.Count != 0) - { - return TypedResults.ValidationProblem(errors); - } - Cuisine cuisine = new() { Name = dto.Name @@ -101,24 +91,12 @@ public static async Task, ValidationProblem, ForbidH } [Authorize] - public static async Task, ValidationProblem, NotFound, ForbidHttpResult>> PutAsync( + public static async Task, NotFound, ForbidHttpResult>> PutAsync( [AsParameters] Services services, ClaimsPrincipal user, int id, CuisineDto dto) { - Dictionary errors = []; - - if (string.IsNullOrWhiteSpace(dto.Name)) - { - errors.Add(nameof(dto.Name), ["Value is required."]); - } - - if (errors.Count != 0) - { - return TypedResults.ValidationProblem(errors); - } - var cuisine = await services.Context.Cuisines .FirstOrDefaultAsync(x => x.Id == id); diff --git a/src/WebApi/Apis/RecipesApi.cs b/src/WebApi/Apis/RecipesApi.cs index 27875ba..5f31fb4 100644 --- a/src/WebApi/Apis/RecipesApi.cs +++ b/src/WebApi/Apis/RecipesApi.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using SharpGrip.FluentValidation.AutoValidation.Endpoints.Extensions; namespace JonathanPotts.RecipeCatalog.WebApi.Apis; @@ -27,6 +28,7 @@ public static class RecipesApi public static IEndpointRouteBuilder MapRecipesApi(this IEndpointRouteBuilder builder) { var group = builder.MapGroup("/api/v1/recipes") + .AddFluentValidationAutoValidation() .WithTags("Recipes"); group.MapGet("/", GetListAsync); @@ -48,12 +50,10 @@ public static async Task>, Validati { if (top is < 1 or > MaxItemsPerPage) { - Dictionary errors = new() + return TypedResults.ValidationProblem(new Dictionary { { nameof(top), [$"Value must be between 1 and {MaxItemsPerPage}."] } - }; - - return TypedResults.ValidationProblem(errors); + }); } IQueryable recipes = services.Context.Recipes; @@ -163,33 +163,11 @@ public static async Task> GetCoverImag } [Authorize] - public static async Task, ValidationProblem, ForbidHttpResult>> PostAsync( + public static async Task, ForbidHttpResult>> PostAsync( [AsParameters] Services services, ClaimsPrincipal user, RecipeCreateOrUpdateDto dto) { - Dictionary errors = []; - - if (string.IsNullOrWhiteSpace(dto.Name)) - { - errors.Add(nameof(dto.Name), ["Value is required."]); - } - - if (dto.Ingredients?.Length == 0 || (dto.Ingredients?.Any(string.IsNullOrWhiteSpace) ?? false)) - { - errors.Add(nameof(dto.Ingredients), ["Value is required."]); - } - - if (string.IsNullOrWhiteSpace(dto.Instructions)) - { - errors.Add(nameof(dto.Instructions), ["Value is required."]); - } - - if (errors.Count != 0) - { - return TypedResults.ValidationProblem(errors); - } - Recipe recipe = new() { Id = services.IdGenerator.CreateId(), @@ -237,34 +215,12 @@ public static async Task, ValidationProble } [Authorize] - public static async Task, ValidationProblem, NotFound, ForbidHttpResult>> PutAsync( + public static async Task, NotFound, ForbidHttpResult>> PutAsync( [AsParameters] Services services, ClaimsPrincipal user, long id, RecipeCreateOrUpdateDto dto) { - Dictionary errors = []; - - if (string.IsNullOrWhiteSpace(dto.Name)) - { - errors.Add(nameof(dto.Name), ["Value is required."]); - } - - if (dto.Ingredients?.Length == 0 || (dto.Ingredients?.Any(string.IsNullOrWhiteSpace) ?? false)) - { - errors.Add(nameof(dto.Ingredients), ["Value is required."]); - } - - if (string.IsNullOrWhiteSpace(dto.Instructions)) - { - errors.Add(nameof(dto.Instructions), ["Value is required."]); - } - - if (errors.Count != 0) - { - return TypedResults.ValidationProblem(errors); - } - var recipe = await services.Context.Recipes .FirstOrDefaultAsync(x => x.Id == id); diff --git a/src/WebApi/Data/EntityConfigurations/CuisineEntityTypeConfiguration.cs b/src/WebApi/Data/EntityConfigurations/CuisineEntityTypeConfiguration.cs index bcd961b..b906b8b 100644 --- a/src/WebApi/Data/EntityConfigurations/CuisineEntityTypeConfiguration.cs +++ b/src/WebApi/Data/EntityConfigurations/CuisineEntityTypeConfiguration.cs @@ -8,6 +8,9 @@ public class CuisineEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { + builder.Property(x => x.Name) + .IsRequired(); + builder.HasIndex(x => x.Name) .IsUnique(); } diff --git a/src/WebApi/Data/EntityConfigurations/RecipeEntityTypeConfiguration.cs b/src/WebApi/Data/EntityConfigurations/RecipeEntityTypeConfiguration.cs index 0b18760..9693862 100644 --- a/src/WebApi/Data/EntityConfigurations/RecipeEntityTypeConfiguration.cs +++ b/src/WebApi/Data/EntityConfigurations/RecipeEntityTypeConfiguration.cs @@ -17,17 +17,45 @@ public void Configure(EntityTypeBuilder builder) x => x != null ? utcConverter.ConvertToProviderTyped(x.Value) : null, x => x != null ? utcConverter.ConvertFromProviderTyped(x.Value) : null); - builder.OwnsOne(x => x.CoverImage); + builder.OwnsOne(x => x.CoverImage, ownedBuilder => + { + ownedBuilder.Property(x => x.Url) + .IsRequired(); + }); - builder.OwnsOne(x => x.Instructions); + builder.OwnsOne(x => x.Instructions, ownedBuilder => + { + ownedBuilder.Property(x => x.Markdown) + .IsRequired(); + + ownedBuilder.Property(x => x.Html) + .IsRequired(); + }); + + builder.Navigation(x => x.Instructions) + .IsRequired(); + + builder.HasOne(x => x.Owner).WithMany() + .HasForeignKey(x => x.OwnerId) + .IsRequired(); + + builder.HasOne(x => x.Cuisine).WithMany(x => x.Recipes) + .HasForeignKey(x => x.CuisineId) + .IsRequired(); builder.Property(x => x.Id) .ValueGeneratedNever(); + builder.Property(x => x.Name) + .IsRequired(); + builder.Property(x => x.Created) .HasConversion(utcConverter); builder.Property(x => x.Modified) .HasConversion(nullableUtcConverter); + + builder.Property(x => x.Ingredients) + .IsRequired(); } } diff --git a/src/WebApi/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/WebApi/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 517c7ff..365d33d 100644 --- a/src/WebApi/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/WebApi/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -32,7 +32,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("Name") .IsUnique(); - b.ToTable("Cuisines"); + b.ToTable("Cuisines", (string)null); }); modelBuilder.Entity("JonathanPotts.RecipeCatalog.WebApi.Models.Recipe", b => @@ -70,7 +70,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OwnerId"); - b.ToTable("Recipes"); + b.ToTable("Recipes", (string)null); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => @@ -279,7 +279,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.OwnsOne("JonathanPotts.RecipeCatalog.WebApi.Models.ImageData", "CoverImage", b1 => + b.OwnsOne("JonathanPotts.RecipeCatalog.WebApi.Models.Recipe.CoverImage#JonathanPotts.RecipeCatalog.WebApi.Models.ImageData", "CoverImage", b1 => { b1.Property("RecipeId") .HasColumnType("INTEGER"); @@ -293,13 +293,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("RecipeId"); - b1.ToTable("Recipes"); + b1.ToTable("Recipes", (string)null); b1.WithOwner() .HasForeignKey("RecipeId"); }); - b.OwnsOne("JonathanPotts.RecipeCatalog.WebApi.Models.MarkdownData", "Instructions", b1 => + b.OwnsOne("JonathanPotts.RecipeCatalog.WebApi.Models.Recipe.Instructions#JonathanPotts.RecipeCatalog.WebApi.Models.MarkdownData", "Instructions", b1 => { b1.Property("RecipeId") .HasColumnType("INTEGER"); @@ -314,7 +314,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("RecipeId"); - b1.ToTable("Recipes"); + b1.ToTable("Recipes", (string)null); b1.WithOwner() .HasForeignKey("RecipeId"); diff --git a/src/WebApi/Models/Cuisine.cs b/src/WebApi/Models/Cuisine.cs index 856a4bb..141bc2b 100644 --- a/src/WebApi/Models/Cuisine.cs +++ b/src/WebApi/Models/Cuisine.cs @@ -1,12 +1,9 @@ -using System.ComponentModel.DataAnnotations; - -namespace JonathanPotts.RecipeCatalog.WebApi.Models; +namespace JonathanPotts.RecipeCatalog.WebApi.Models; public class Cuisine { public int Id { get; set; } - [Required] public string? Name { get; set; } public List? Recipes { get; set; } diff --git a/src/WebApi/Models/CuisineCreateOrUpdateDto.cs b/src/WebApi/Models/CuisineCreateOrUpdateDto.cs index bb23a97..4489802 100644 --- a/src/WebApi/Models/CuisineCreateOrUpdateDto.cs +++ b/src/WebApi/Models/CuisineCreateOrUpdateDto.cs @@ -1,9 +1,6 @@ -using System.ComponentModel.DataAnnotations; - -namespace JonathanPotts.RecipeCatalog.WebApi.Models; +namespace JonathanPotts.RecipeCatalog.WebApi.Models; public class CuisineCreateOrUpdateDto { - [Required] public string? Name { get; set; } } diff --git a/src/WebApi/Models/CuisineDto.cs b/src/WebApi/Models/CuisineDto.cs index 5f2a463..06cb80e 100644 --- a/src/WebApi/Models/CuisineDto.cs +++ b/src/WebApi/Models/CuisineDto.cs @@ -1,11 +1,8 @@ -using System.ComponentModel.DataAnnotations; - -namespace JonathanPotts.RecipeCatalog.WebApi.Models; +namespace JonathanPotts.RecipeCatalog.WebApi.Models; public class CuisineDto { public int Id { get; set; } - [Required] public string? Name { get; set; } } diff --git a/src/WebApi/Models/ImageData.cs b/src/WebApi/Models/ImageData.cs index fb4f947..3d5b45b 100644 --- a/src/WebApi/Models/ImageData.cs +++ b/src/WebApi/Models/ImageData.cs @@ -1,10 +1,7 @@ -using System.ComponentModel.DataAnnotations; - -namespace JonathanPotts.RecipeCatalog.WebApi.Models; +namespace JonathanPotts.RecipeCatalog.WebApi.Models; public class ImageData { - [Required] public string? Url { get; set; } public string? AltText { get; set; } diff --git a/src/WebApi/Models/MarkdownData.cs b/src/WebApi/Models/MarkdownData.cs index feaae89..fbdcc59 100644 --- a/src/WebApi/Models/MarkdownData.cs +++ b/src/WebApi/Models/MarkdownData.cs @@ -1,12 +1,8 @@ -using System.ComponentModel.DataAnnotations; - -namespace JonathanPotts.RecipeCatalog.WebApi.Models; +namespace JonathanPotts.RecipeCatalog.WebApi.Models; public class MarkdownData { - [Required] public string? Markdown { get; set; } - [Required] public string? Html { get; set; } } diff --git a/src/WebApi/Models/Recipe.cs b/src/WebApi/Models/Recipe.cs index c4347ef..381430d 100644 --- a/src/WebApi/Models/Recipe.cs +++ b/src/WebApi/Models/Recipe.cs @@ -1,5 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; namespace JonathanPotts.RecipeCatalog.WebApi.Models; @@ -7,20 +6,16 @@ public class Recipe { public long Id { get; set; } - [Required] public string? OwnerId { get; set; } - [Required] public IdentityUser? Owner { get; set; } - [Required] public string? Name { get; set; } public ImageData? CoverImage { get; set; } public int CuisineId { get; set; } - [Required] public Cuisine? Cuisine { get; set; } public string? Description { get; set; } @@ -29,9 +24,7 @@ public class Recipe public DateTime? Modified { get; set; } - [Required] public string[]? Ingredients { get; set; } - [Required] public MarkdownData? Instructions { get; set; } } diff --git a/src/WebApi/Models/RecipeCreateOrUpdateDto.cs b/src/WebApi/Models/RecipeCreateOrUpdateDto.cs index 163d65a..ffe6183 100644 --- a/src/WebApi/Models/RecipeCreateOrUpdateDto.cs +++ b/src/WebApi/Models/RecipeCreateOrUpdateDto.cs @@ -1,19 +1,14 @@ -using System.ComponentModel.DataAnnotations; - -namespace JonathanPotts.RecipeCatalog.WebApi.Models; +namespace JonathanPotts.RecipeCatalog.WebApi.Models; public class RecipeCreateOrUpdateDto { - [Required] public string? Name { get; set; } public int CuisineId { get; set; } public string? Description { get; set; } - [Required] public string[]? Ingredients { get; set; } - [Required] public string? Instructions { get; set; } } diff --git a/src/WebApi/Models/RecipeDto.cs b/src/WebApi/Models/RecipeDto.cs index 6ac2e13..4879590 100644 --- a/src/WebApi/Models/RecipeDto.cs +++ b/src/WebApi/Models/RecipeDto.cs @@ -1,5 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace JonathanPotts.RecipeCatalog.WebApi.Models; @@ -8,10 +7,8 @@ public class RecipeDto [JsonNumberHandling(JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString)] public long Id { get; set; } - [Required] public string? OwnerId { get; set; } - [Required] public string? Name { get; set; } public ImageData? CoverImage { get; set; } diff --git a/src/WebApi/Models/RecipeWithCuisineDto.cs b/src/WebApi/Models/RecipeWithCuisineDto.cs index 9b750e7..0b2fafa 100644 --- a/src/WebApi/Models/RecipeWithCuisineDto.cs +++ b/src/WebApi/Models/RecipeWithCuisineDto.cs @@ -1,9 +1,6 @@ -using System.ComponentModel.DataAnnotations; - -namespace JonathanPotts.RecipeCatalog.WebApi.Models; +namespace JonathanPotts.RecipeCatalog.WebApi.Models; public class RecipeWithCuisineDto : RecipeDto { - [Required] public CuisineDto? Cuisine { get; set; } } diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 6953712..8627cd5 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -1,8 +1,10 @@ +using FluentValidation; using IdGen; using IdGen.DependencyInjection; using JonathanPotts.RecipeCatalog.WebApi.Apis; using JonathanPotts.RecipeCatalog.WebApi.Authorization; using JonathanPotts.RecipeCatalog.WebApi.Data; +using MicroElements.Swashbuckle.FluentValidation.AspNetCore; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -24,12 +26,16 @@ builder.Services.AddScoped(); +builder.Services.AddValidatorsFromAssemblyContaining(); + builder.Services.AddProblemDetails(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddFluentValidationRulesToSwagger(); + // Add IdGen for creating Snowflake IDs var generatorId = builder.Configuration.GetValue("GeneratorId", 0); var epoch = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); diff --git a/src/WebApi/README.md b/src/WebApi/README.md index 5535abd..d8dad91 100644 --- a/src/WebApi/README.md +++ b/src/WebApi/README.md @@ -9,6 +9,7 @@ WebApi is a REST Web API that provides CRUD operations for managing recipes. Tec - [Entity Framework Core](https://learn.microsoft.com/ef/core/) - [Migrations](https://learn.microsoft.com/ef/core/managing-schemas/migrations/) - [SQLite](https://www.sqlite.org/) +- [FluentValidation](https://github.com/FluentValidation/FluentValidation) - Markdown ([Markdig](https://github.com/xoofx/markdig)) - Snowflake IDs ([IdGen](https://github.com/RobThree/IdGen)) diff --git a/src/WebApi/Validation/CuisineCreateOrUpdateDtoValidator.cs b/src/WebApi/Validation/CuisineCreateOrUpdateDtoValidator.cs new file mode 100644 index 0000000..789333c --- /dev/null +++ b/src/WebApi/Validation/CuisineCreateOrUpdateDtoValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using JonathanPotts.RecipeCatalog.WebApi.Models; + +namespace JonathanPotts.RecipeCatalog.WebApi.Validation; + +public class CuisineCreateOrUpdateDtoValidator : AbstractValidator +{ + public CuisineCreateOrUpdateDtoValidator() + { + RuleFor(x => x.Name).NotEmpty(); + } +} diff --git a/src/WebApi/Validation/CuisineDtoValidator.cs b/src/WebApi/Validation/CuisineDtoValidator.cs new file mode 100644 index 0000000..8be79cd --- /dev/null +++ b/src/WebApi/Validation/CuisineDtoValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using JonathanPotts.RecipeCatalog.WebApi.Models; + +namespace JonathanPotts.RecipeCatalog.WebApi.Validation; + +public class CuisineDtoValidator : AbstractValidator +{ + public CuisineDtoValidator() + { + RuleFor(x => x.Id).NotEmpty(); + RuleFor(x => x.Name).NotEmpty(); + } +} diff --git a/src/WebApi/Validation/CuisineValidator.cs b/src/WebApi/Validation/CuisineValidator.cs new file mode 100644 index 0000000..b9df298 --- /dev/null +++ b/src/WebApi/Validation/CuisineValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using JonathanPotts.RecipeCatalog.WebApi.Models; + +namespace JonathanPotts.RecipeCatalog.WebApi.Validation; + +public class CuisineValidator : AbstractValidator +{ + public CuisineValidator() + { + RuleFor(x => x.Name).NotEmpty(); + } +} diff --git a/src/WebApi/Validation/ImageDataValidator.cs b/src/WebApi/Validation/ImageDataValidator.cs new file mode 100644 index 0000000..8f85fd0 --- /dev/null +++ b/src/WebApi/Validation/ImageDataValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using JonathanPotts.RecipeCatalog.WebApi.Models; + +namespace JonathanPotts.RecipeCatalog.WebApi.Validation; + +public class ImageDataValidator : AbstractValidator +{ + public ImageDataValidator() + { + RuleFor(x => x.Url).NotEmpty(); + } +} diff --git a/src/WebApi/Validation/MarkdownDataValidator.cs b/src/WebApi/Validation/MarkdownDataValidator.cs new file mode 100644 index 0000000..f4a97e7 --- /dev/null +++ b/src/WebApi/Validation/MarkdownDataValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using JonathanPotts.RecipeCatalog.WebApi.Models; + +namespace JonathanPotts.RecipeCatalog.WebApi.Validation; + +public class MarkdownDataValidator : AbstractValidator +{ + public MarkdownDataValidator() + { + RuleFor(x => x.Markdown).NotEmpty(); + RuleFor(x => x.Html).NotEmpty(); + } +} diff --git a/src/WebApi/Validation/RecipeCreateOrUpdateDtoValidator.cs b/src/WebApi/Validation/RecipeCreateOrUpdateDtoValidator.cs new file mode 100644 index 0000000..996f856 --- /dev/null +++ b/src/WebApi/Validation/RecipeCreateOrUpdateDtoValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using JonathanPotts.RecipeCatalog.WebApi.Models; + +namespace JonathanPotts.RecipeCatalog.WebApi.Validation; + +public class RecipeCreateOrUpdateDtoValidator : AbstractValidator +{ + public RecipeCreateOrUpdateDtoValidator() + { + RuleFor(x => x.Name).NotEmpty(); + RuleFor(x => x.CuisineId).NotEmpty(); + RuleFor(x => x.Ingredients).NotEmpty(); + RuleFor(x => x.Instructions).NotEmpty(); + } +} diff --git a/src/WebApi/Validation/RecipeDtoValidator.cs b/src/WebApi/Validation/RecipeDtoValidator.cs new file mode 100644 index 0000000..02faac0 --- /dev/null +++ b/src/WebApi/Validation/RecipeDtoValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using JonathanPotts.RecipeCatalog.WebApi.Models; + +namespace JonathanPotts.RecipeCatalog.WebApi.Validation; + +public class RecipeDtoValidator : AbstractValidator +{ + public RecipeDtoValidator() + { + RuleFor(x => x.Id).NotEmpty(); + RuleFor(x => x.OwnerId).NotEmpty(); + RuleFor(x => x.Name).NotEmpty(); + } +} diff --git a/src/WebApi/Validation/RecipeValidator.cs b/src/WebApi/Validation/RecipeValidator.cs new file mode 100644 index 0000000..f13263e --- /dev/null +++ b/src/WebApi/Validation/RecipeValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using JonathanPotts.RecipeCatalog.WebApi.Models; + +namespace JonathanPotts.RecipeCatalog.WebApi.Validation; + +public class RecipeValidator : AbstractValidator +{ + public RecipeValidator() + { + RuleFor(x => x.OwnerId).NotEmpty(); + RuleFor(x => x.Name).NotEmpty(); + RuleFor(x => x.CuisineId).NotEmpty(); + RuleFor(x => x.Ingredients).NotEmpty(); + RuleFor(x => x.Ingredients).NotEmpty(); + } +} diff --git a/src/WebApi/Validation/RecipeWithCuisineDtoValidator.cs b/src/WebApi/Validation/RecipeWithCuisineDtoValidator.cs new file mode 100644 index 0000000..ffcfe86 --- /dev/null +++ b/src/WebApi/Validation/RecipeWithCuisineDtoValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using JonathanPotts.RecipeCatalog.WebApi.Models; + +namespace JonathanPotts.RecipeCatalog.WebApi.Validation; + +public class RecipeWithCuisineDtoValidator : AbstractValidator +{ + public RecipeWithCuisineDtoValidator() + { + RuleFor(x => x.Cuisine).NotEmpty(); + } +} diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index 692e764..e7eddad 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -15,9 +15,12 @@ + + + @@ -25,6 +28,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/WebApi.Tests/Apis/RecipesApiUnitTests.cs b/tests/WebApi.Tests/Apis/RecipesApiUnitTests.cs index 8c73566..d58c277 100644 --- a/tests/WebApi.Tests/Apis/RecipesApiUnitTests.cs +++ b/tests/WebApi.Tests/Apis/RecipesApiUnitTests.cs @@ -325,16 +325,6 @@ public async void PostAsyncReturnsRecipeForValidModel() () => Assert.Equal("

This is new.

\n", createdResult.Value.Instructions?.Html)); } - [Fact] - public async void PostAsyncReturnsValidationProblemForInvalidModel() - { - // Act - var result = await RecipesApi.PostAsync(_services, _user, new RecipeCreateOrUpdateDto()); - - // Assert - Assert.IsType(result.Result); - } - [Fact] public async void PostAsyncReturnsForbidForAnonymousUser() { @@ -411,16 +401,6 @@ public async void PutAsyncReturnsArticleForValidModel() () => Assert.Equal("

This is updated.

\n", okResult.Value.Instructions?.Html)); } - [Fact] - public async void PutAsyncReturnsValidationProblemForInvalidModel() - { - // Act - var result = await RecipesApi.PutAsync(_services, _adminUser, 6462416804118528, new RecipeCreateOrUpdateDto()); - - // Assert - Assert.IsType(result.Result); - } - [Fact] public async void PutAsyncReturnsNotFoundForInvalidId() {