diff --git a/.idea/.idea.Fin-Backend/.idea/dataSources.xml b/.idea/.idea.Fin-Backend/.idea/dataSources.xml index 5f87bc2..cc31fb4 100644 --- a/.idea/.idea.Fin-Backend/.idea/dataSources.xml +++ b/.idea/.idea.Fin-Backend/.idea/dataSources.xml @@ -1,12 +1,11 @@ - + postgresql true - true org.postgresql.Driver - jdbc:postgresql://localhost:5432/postgres?password=postgres&user=postgres + jdbc:postgresql://localhost:5432/postgres $ProjectFileDir$ diff --git a/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml b/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml deleted file mode 100644 index 3e9887f..0000000 --- a/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Fin-Backend.sln.DotSettings.user b/Fin-Backend.sln.DotSettings.user index 67f132d..1d58c68 100644 --- a/Fin-Backend.sln.DotSettings.user +++ b/Fin-Backend.sln.DotSettings.user @@ -1,4 +1,9 @@  - <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + ForceIncluded + ForceIncluded + ForceIncluded + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Solution /> -</SessionState> \ No newline at end of file +</SessionState> + + \ No newline at end of file diff --git a/Fin.Api/TitleCategories/TitleCategoryController.cs b/Fin.Api/TitleCategories/TitleCategoryController.cs new file mode 100644 index 0000000..3a66689 --- /dev/null +++ b/Fin.Api/TitleCategories/TitleCategoryController.cs @@ -0,0 +1,54 @@ +using Fin.Application.TitleCategories; +using Fin.Application.TitleCategories.Dtos; +using Fin.Domain.Global.Classes; +using Fin.Domain.TitleCategories.Dtos; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Fin.Api.TitleCategories; + +[Route("title-categories")] +[Authorize] +public class TitleCategoryController(ITitleCategoryService service): ControllerBase +{ + [HttpGet] + public async Task> GetList([FromQuery] TitleCategoryGetListInput input) + { + return await service.GetList(input); + } + + [HttpGet("{id:guid}")] + public async Task> Get([FromRoute] Guid id) + { + var category = await service.Get(id); + return category != null ? Ok(category) : NotFound(); + } + + [HttpPost] + public async Task> Create([FromBody] TitleCategoryInput input) + { + var category = await service.Create(input, autoSave: true); + return category != null ? Created($"categories/{category.Id}", category) : UnprocessableEntity(); + } + + [HttpPut("{id:guid}")] + public async Task Update([FromRoute] Guid id, [FromBody] TitleCategoryInput input) + { + var updated = await service.Update(id, input, autoSave: true); + return updated ? Ok() : UnprocessableEntity(); + } + + [HttpPut("toggle-inactivated/{id:guid}")] + public async Task ToggleInactivated([FromRoute] Guid id) + { + var updated = await service.ToggleInactive(id, autoSave: true); + return updated ? Ok() : UnprocessableEntity(); + } + + [HttpDelete("{id:guid}")] + public async Task Delete([FromRoute] Guid id) + { + var deleted = await service.Delete(id, autoSave: true); + return deleted ? Ok() : UnprocessableEntity(); + } +} \ No newline at end of file diff --git a/Fin.Application/TitleCategories/Dtos/TitleCategoryGetListInput.cs b/Fin.Application/TitleCategories/Dtos/TitleCategoryGetListInput.cs new file mode 100644 index 0000000..6064e77 --- /dev/null +++ b/Fin.Application/TitleCategories/Dtos/TitleCategoryGetListInput.cs @@ -0,0 +1,10 @@ +using Fin.Domain.Global.Classes; +using Fin.Domain.TitleCategories.Enums; + +namespace Fin.Application.TitleCategories.Dtos; + +public class TitleCategoryGetListInput: PagedFilteredAndSortedInput +{ + public bool? Inactivated { get; set; } + public TitleCategoryType? Type { get; set; } +} \ No newline at end of file diff --git a/Fin.Application/TitleCategories/TitleCategoryService.cs b/Fin.Application/TitleCategories/TitleCategoryService.cs new file mode 100644 index 0000000..d32ffff --- /dev/null +++ b/Fin.Application/TitleCategories/TitleCategoryService.cs @@ -0,0 +1,97 @@ +using Fin.Application.TitleCategories.Dtos; +using Fin.Domain.Global.Classes; +using Fin.Domain.TitleCategories.Dtos; +using Fin.Domain.TitleCategories.Entities; +using Fin.Infrastructure.AutoServices.Interfaces; +using Fin.Infrastructure.Database.Extensions; +using Fin.Infrastructure.Database.Repositories; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; + +namespace Fin.Application.TitleCategories; + +public interface ITitleCategoryService +{ + public Task Get(Guid id); + public Task> GetList(TitleCategoryGetListInput input); + public Task Create(TitleCategoryInput input, bool autoSave = false); + public Task Update(Guid id, TitleCategoryInput input, bool autoSave = false); + public Task Delete(Guid id, bool autoSave = false); + public Task ToggleInactive(Guid id, bool autoSave = false); +} + +public class TitleCategoryService( + IRepository repository + ) : ITitleCategoryService, IAutoTransient +{ + public async Task Get(Guid id) + { + var entity = await repository.Query(false).FirstOrDefaultAsync(n => n.Id == id); + return entity != null ? new TitleCategoryOutput(entity) : null; + } + + public async Task> GetList(TitleCategoryGetListInput input) + { + return await repository.Query(false) + .WhereIf(input.Inactivated.HasValue, n => n.Inactivated == input.Inactivated.Value) + .WhereIf(input.Type.HasValue, n => n.Type == input.Type.Value) + .OrderBy(m => m.Inactivated) + .ThenBy(m => m.Name) + .ApplyFilterAndSorter(input) + .Select(n => new TitleCategoryOutput(n)) + .ToPagedResult(input); + } + + public async Task Create(TitleCategoryInput input, bool autoSave = false) + { + ValidarInput(input); + var titleCategory = new TitleCategory(input); + await repository.AddAsync(titleCategory, autoSave); + return new TitleCategoryOutput(titleCategory); + } + + public async Task Update(Guid id, TitleCategoryInput input, bool autoSave = false) + { + ValidarInput(input); + var titleCategory = await repository.Query() + .FirstOrDefaultAsync(u => u.Id == id); + if (titleCategory == null) return false; + + titleCategory.Update(input); + await repository.UpdateAsync(titleCategory, autoSave); + + return true; + } + + public async Task Delete(Guid id, bool autoSave = false) + { + var titleCategory = await repository.Query() + .FirstOrDefaultAsync(u => u.Id == id); + if (titleCategory == null) return false; + + await repository.DeleteAsync(titleCategory, autoSave); + return true; + } + + public async Task ToggleInactive(Guid id, bool autoSave = false) + { + var titleCategory = await repository.Query() + .FirstOrDefaultAsync(u => u.Id == id); + if (titleCategory == null) return false; + + titleCategory.ToggleInactivated(); + await repository.UpdateAsync(titleCategory, autoSave); + + return true; + } + + private static void ValidarInput( TitleCategoryInput input) + { + if (string.IsNullOrWhiteSpace(input.Color)) + throw new BadHttpRequestException("FrontRoute is required"); + if (string.IsNullOrWhiteSpace(input.Name)) + throw new BadHttpRequestException("Name is required"); + if (string.IsNullOrWhiteSpace(input.Icon)) + throw new BadHttpRequestException("Icon is required"); + } +} \ No newline at end of file diff --git a/Fin.Domain/Global/Interfaces/IAuditedEntity.cs b/Fin.Domain/Global/Interfaces/IAuditedEntity.cs index cbde6a6..570e202 100644 --- a/Fin.Domain/Global/Interfaces/IAuditedEntity.cs +++ b/Fin.Domain/Global/Interfaces/IAuditedEntity.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations; - -namespace Fin.Domain.Global.Interfaces; +namespace Fin.Domain.Global.Interfaces; public interface IAuditedEntity: IEntity { diff --git a/Fin.Domain/TitleCategories/Dtos/TitleCategoryInput.cs b/Fin.Domain/TitleCategories/Dtos/TitleCategoryInput.cs new file mode 100644 index 0000000..965046b --- /dev/null +++ b/Fin.Domain/TitleCategories/Dtos/TitleCategoryInput.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using Fin.Domain.TitleCategories.Enums; + +namespace Fin.Domain.TitleCategories.Dtos; + +public class TitleCategoryInput +{ + [Required] + public string Name { get; set; } + + [Required] + public string Color { get; set; } + [Required] + + public string Icon { get; set; } + + [Required] + public TitleCategoryType Type { get; set; } +} \ No newline at end of file diff --git a/Fin.Domain/TitleCategories/Dtos/TitleCategoryOutput.cs b/Fin.Domain/TitleCategories/Dtos/TitleCategoryOutput.cs new file mode 100644 index 0000000..523f3eb --- /dev/null +++ b/Fin.Domain/TitleCategories/Dtos/TitleCategoryOutput.cs @@ -0,0 +1,19 @@ +using Fin.Domain.TitleCategories.Entities; +using Fin.Domain.TitleCategories.Enums; + +namespace Fin.Domain.TitleCategories.Dtos; + +public class TitleCategoryOutput(TitleCategory titleCategory) +{ + public Guid Id { get; set; } = titleCategory.Id; + public bool Inactivated { get; set; } = titleCategory.Inactivated; + public string Name { get; set; } = titleCategory.Name; + public string Color { get; set; } = titleCategory.Color; + public string Icon { get; set; } = titleCategory.Icon; + public TitleCategoryType Type { get; set; } = titleCategory.Type; + + public TitleCategoryOutput(): this(new TitleCategory()) + { + } + +} \ No newline at end of file diff --git a/Fin.Domain/TitleCategories/Entities/TitleCategory.cs b/Fin.Domain/TitleCategories/Entities/TitleCategory.cs new file mode 100644 index 0000000..605bbe2 --- /dev/null +++ b/Fin.Domain/TitleCategories/Entities/TitleCategory.cs @@ -0,0 +1,42 @@ +using Fin.Domain.Global.Interfaces; +using Fin.Domain.TitleCategories.Dtos; +using Fin.Domain.TitleCategories.Enums; + +namespace Fin.Domain.TitleCategories.Entities; + +public class TitleCategory: IAuditedTenantEntity +{ + public bool Inactivated { get; private set; } + public string Name { get; private set; } + public string Color { get; private set; } + public string Icon { get; private set; } + public TitleCategoryType Type { get; private set; } + + public Guid Id { get; set; } + public Guid CreatedBy { get; set; } + public Guid UpdatedBy { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public Guid TenantId { get; set; } + + public TitleCategory() + { + } + public TitleCategory(TitleCategoryInput input) + { + Name = input.Name; + Color = input.Color; + Icon = input.Icon; + Type = input.Type; + } + + public void Update(TitleCategoryInput input) + { + Name = input.Name; + Color = input.Color; + Icon = input.Icon; + Type = input.Type; + } + + public void ToggleInactivated() => Inactivated = !Inactivated; +} \ No newline at end of file diff --git a/Fin.Domain/TitleCategories/Enums/TitleCategoryType.cs b/Fin.Domain/TitleCategories/Enums/TitleCategoryType.cs new file mode 100644 index 0000000..398ee35 --- /dev/null +++ b/Fin.Domain/TitleCategories/Enums/TitleCategoryType.cs @@ -0,0 +1,8 @@ +namespace Fin.Domain.TitleCategories.Enums; + +public enum TitleCategoryType: byte +{ + Expense = 0, + Income = 1, + Both = 2 +} \ No newline at end of file diff --git a/Fin.Infrastructure/Database/Configurations/TitleCategories/TitleCategoryConfiguration.cs b/Fin.Infrastructure/Database/Configurations/TitleCategories/TitleCategoryConfiguration.cs new file mode 100644 index 0000000..555d78b --- /dev/null +++ b/Fin.Infrastructure/Database/Configurations/TitleCategories/TitleCategoryConfiguration.cs @@ -0,0 +1,17 @@ +using Fin.Domain.TitleCategories.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Fin.Infrastructure.Database.Configurations.TitleCategories; + +public class TitleCategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + + builder.Property(x => x.Name).HasMaxLength(100).IsRequired(); + builder.Property(x => x.Icon).HasMaxLength(20).IsRequired(); + builder.Property(x => x.Color).HasMaxLength(20).IsRequired(); + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Database/FinDbContext.cs b/Fin.Infrastructure/Database/FinDbContext.cs index 527c06c..a91f6bb 100644 --- a/Fin.Infrastructure/Database/FinDbContext.cs +++ b/Fin.Infrastructure/Database/FinDbContext.cs @@ -4,6 +4,7 @@ using Fin.Domain.Notifications; using Fin.Domain.Notifications.Entities; using Fin.Domain.Tenants.Entities; +using Fin.Domain.TitleCategories.Entities; using Fin.Domain.Users.Entities; using Fin.Infrastructure.AmbientDatas; using Fin.Infrastructure.Database.Configurations; @@ -27,6 +28,8 @@ public class FinDbContext : DbContext public DbSet NotificationUserDeliveries { get; set; } public DbSet Menus { get; set; } + + public DbSet TitleCategories { get; set; } private readonly IAmbientData _ambientData; @@ -39,6 +42,8 @@ public FinDbContext(DbContextOptions options, IAmbientData ambient { _ambientData = ambientData; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -61,7 +66,6 @@ protected override void OnConfiguring(DbContextOptionsBuilder options) private void ApplyTenantFilter(ModelBuilder modelBuilder) { - if (!(_ambientData?.IsLogged ?? false)) return; foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType)) @@ -77,7 +81,8 @@ private void ApplyTenantFilter(ModelBuilder modelBuilder) private void SetTenantFilter(ModelBuilder modelBuilder) where TEntity : class, ITenantEntity { - modelBuilder.Entity().HasQueryFilter(e => e.TenantId == _ambientData.TenantId); + if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") return; + modelBuilder.Entity().HasQueryFilter(e => _ambientData.IsLogged && e.TenantId == _ambientData.TenantId); } private void ApplyUtcConverterToDateTime(ModelBuilder modelBuilder) diff --git a/Fin.Infrastructure/Migrations/20251004230243_adding_title_category.Designer.cs b/Fin.Infrastructure/Migrations/20251004230243_adding_title_category.Designer.cs new file mode 100644 index 0000000..23e8553 --- /dev/null +++ b/Fin.Infrastructure/Migrations/20251004230243_adding_title_category.Designer.cs @@ -0,0 +1,560 @@ +// +using System; +using Fin.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Fin.Infrastructure.Migrations +{ + [DbContext(typeof(FinDbContext))] + [Migration("20251004230243_adding_title_category")] + partial class adding_title_category + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Fin.Domain.Menus.Entities.Menu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("FrontRoute") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("KeyWords") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OnlyForAdmin") + .HasColumnType("boolean"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Menus", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Continuous") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("HtmlBody") + .HasColumnType("text"); + + b.Property("Link") + .HasColumnType("text"); + + b.Property("NormalizedTextBody") + .HasColumnType("text"); + + b.Property("NormalizedTitle") + .HasColumnType("text"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.Property("StartToDelivery") + .HasColumnType("timestamp with time zone"); + + b.Property("StopToDelivery") + .HasColumnType("timestamp with time zone"); + + b.Property("TextBody") + .HasColumnType("text"); + + b.Property("Title") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Ways") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Notifications", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.NotificationUserDelivery", b => + { + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("BackgroundJobId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Delivery") + .HasColumnType("boolean"); + + b.Property("Visualized") + .HasColumnType("boolean"); + + b.HasKey("NotificationId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationUserDeliveries", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserNotificationSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowedWays") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("FirebaseTokens") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserNotificationSettings", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserRememberUseSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("NotifyOn") + .HasColumnType("interval"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Ways") + .HasColumnType("text"); + + b.Property("WeekDays") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRememberUseSettings", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Tenants.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Locale") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Timezone") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tenants", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Tenants.Entities.TenantUser", b => + { + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TenantId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("TenantUsers", "public"); + }); + + modelBuilder.Entity("Fin.Domain.TitleCategories.Entities.TitleCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Inactivated") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("TitleCategories", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("FirstName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("ImagePublicUrl") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActivity") + .HasColumnType("boolean"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Users", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.UserCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EncryptedEmail") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EncryptedPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("FailLoginAttempts") + .HasColumnType("integer"); + + b.Property("GoogleId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ResetToken") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EncryptedEmail") + .IsUnique(); + + b.HasIndex("GoogleId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Credentials", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.UserDeleteRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Aborted") + .HasColumnType("boolean"); + + b.Property("AbortedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeleteEffectivatedAt") + .HasColumnType("date"); + + b.Property("DeleteRequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserAbortedId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserAbortedId"); + + b.HasIndex("UserId"); + + b.ToTable("UserDeleteRequests", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.NotificationUserDelivery", b => + { + b.HasOne("Fin.Domain.Notifications.Entities.Notification", "Notification") + .WithMany("UserDeliveries") + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Fin.Domain.Users.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserNotificationSettings", b => + { + b.HasOne("Fin.Domain.Users.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserRememberUseSetting", b => + { + b.HasOne("Fin.Domain.Users.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Fin.Domain.Tenants.Entities.TenantUser", b => + { + b.HasOne("Fin.Domain.Tenants.Entities.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Fin.Domain.Users.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.UserCredential", b => + { + b.HasOne("Fin.Domain.Users.Entities.User", "User") + .WithOne("Credential") + .HasForeignKey("Fin.Domain.Users.Entities.UserCredential", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.UserDeleteRequest", b => + { + b.HasOne("Fin.Domain.Users.Entities.User", "UserAborted") + .WithMany() + .HasForeignKey("UserAbortedId"); + + b.HasOne("Fin.Domain.Users.Entities.User", "User") + .WithMany("DeleteRequests") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserAborted"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.Notification", b => + { + b.Navigation("UserDeliveries"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.User", b => + { + b.Navigation("Credential"); + + b.Navigation("DeleteRequests"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Fin.Infrastructure/Migrations/20251004230243_adding_title_category.cs b/Fin.Infrastructure/Migrations/20251004230243_adding_title_category.cs new file mode 100644 index 0000000..ce2ee63 --- /dev/null +++ b/Fin.Infrastructure/Migrations/20251004230243_adding_title_category.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Fin.Infrastructure.Migrations +{ + /// + public partial class adding_title_category : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TitleCategories", + schema: "public", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Inactivated = table.Column(type: "boolean", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Color = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Icon = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Type = table.Column(type: "smallint", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TitleCategories", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TitleCategories", + schema: "public"); + } + } +} diff --git a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs index 1e384e8..0240f1f 100644 --- a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs +++ b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs @@ -277,6 +277,53 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("TenantUsers", "public"); }); + modelBuilder.Entity("Fin.Domain.TitleCategories.Entities.TitleCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Inactivated") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("TitleCategories", "public"); + }); + modelBuilder.Entity("Fin.Domain.Users.Entities.User", b => { b.Property("Id") diff --git a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs index e211704..9478140 100644 --- a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs +++ b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs @@ -18,6 +18,17 @@ public async Task SeedAsync() var defaultMenus = new List { + new() + { + Id = Guid.Parse("0199b289-82a7-7069-9230-05250b55fd47"), + FrontRoute = "/title-categories", + Name = "finCore.features.titleCategory.title", + Color = "#fdc570", + Icon = "icons", + OnlyForAdmin = false, + Position = MenuPosition.LeftTop, + KeyWords = "title category, categória do título, categoria" + }, new() { Id = Guid.Parse("01994133-6669-7fcd-b6db-19a9b0c06f20"), diff --git a/Fin.Test/TitleCategories/Controllers/TitleCategoryControllerTest.cs b/Fin.Test/TitleCategories/Controllers/TitleCategoryControllerTest.cs new file mode 100644 index 0000000..7303c7c --- /dev/null +++ b/Fin.Test/TitleCategories/Controllers/TitleCategoryControllerTest.cs @@ -0,0 +1,213 @@ +using Fin.Api.TitleCategories; +using Fin.Application.TitleCategories; +using Fin.Application.TitleCategories.Dtos; +using Fin.Domain.Global.Classes; +using Fin.Domain.TitleCategories.Dtos; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Moq; + +namespace Fin.Test.TitleCategories.Controllers; + +public class TitleCategoryControllerTest : TestUtils.BaseTest +{ + private readonly Mock _serviceMock; + private readonly TitleCategoryController _controller; + + public TitleCategoryControllerTest() + { + _serviceMock = new Mock(); + _controller = new TitleCategoryController(_serviceMock.Object); + } + + #region GetList + + [Fact] + public async Task GetList_ShouldReturnPagedOutput() + { + // Arrange + var input = new TitleCategoryGetListInput(); + var expectedOutput = new PagedOutput(1, + [ + new TitleCategoryOutput { Id = TestUtils.Guids[0], Name = TestUtils.Strings[1] } + ]); + + _serviceMock.Setup(s => s.GetList(input)).ReturnsAsync(expectedOutput); + + // Act + var result = await _controller.GetList(input); + + // Assert + result.Should().BeEquivalentTo(expectedOutput); + } + + #endregion + + #region Get + + [Fact] + public async Task Get_ShouldReturnOk_WhenCategoryExists() + { + // Arrange + var categoryId = TestUtils.Guids[0]; + var expectedCategory = new TitleCategoryOutput { Id = categoryId, Name = TestUtils.Strings[1] }; + _serviceMock.Setup(s => s.Get(categoryId)).ReturnsAsync(expectedCategory); + + // Act + var result = await _controller.Get(categoryId); + + // Assert + result.Result.Should().BeOfType() + .Which.Value.Should().Be(expectedCategory); + } + + [Fact] + public async Task Get_ShouldReturnNotFound_WhenCategoryDoesNotExist() + { + // Arrange + var categoryId = TestUtils.Guids[0]; + _serviceMock.Setup(s => s.Get(categoryId)).ReturnsAsync((TitleCategoryOutput)null); + + // Act + var result = await _controller.Get(categoryId); + + // Assert + result.Result.Should().BeOfType(); + } + + #endregion + + #region Create + + [Fact] + public async Task Create_ShouldReturnCreated_WhenInputIsValid() + { + // Arrange + var input = new TitleCategoryInput { Name = TestUtils.Strings[1], Color = TestUtils.Strings[2], Icon = TestUtils.Strings[3] }; + var createdCategory = new TitleCategoryOutput { Id = TestUtils.Guids[0], Name = TestUtils.Strings[1] }; + _serviceMock.Setup(s => s.Create(input, true)).ReturnsAsync(createdCategory); + + // Act + var result = await _controller.Create(input); + + // Assert + result.Result.Should().BeOfType() + .Which.Value.Should().Be(createdCategory); + + (result.Result as CreatedResult)?.Location.Should().Be($"categories/{createdCategory.Id}"); + } + + [Fact] + public async Task Create_ShouldReturnUnprocessableEntity_WhenCreationFails() + { + // Arrange + var input = new TitleCategoryInput(); + _serviceMock.Setup(s => s.Create(input, true)).ReturnsAsync((TitleCategoryOutput)null); + + // Act + var result = await _controller.Create(input); + + // Assert + result.Result.Should().BeOfType(); + } + + #endregion + + #region Update + + [Fact] + public async Task Update_ShouldReturnOk_WhenUpdateSucceeds() + { + // Arrange + var categoryId = TestUtils.Guids[0]; + var input = new TitleCategoryInput { Name = TestUtils.Strings[1], Color = TestUtils.Strings[2], Icon = TestUtils.Strings[3] }; + _serviceMock.Setup(s => s.Update(categoryId, input, true)).ReturnsAsync(true); + + // Act + var result = await _controller.Update(categoryId, input); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task Update_ShouldReturnUnprocessableEntity_WhenUpdateFails() + { + // Arrange + var categoryId = TestUtils.Guids[0]; + var input = new TitleCategoryInput(); + _serviceMock.Setup(s => s.Update(categoryId, input, true)).ReturnsAsync(false); + + // Act + var result = await _controller.Update(categoryId, input); + + // Assert + result.Should().BeOfType(); + } + + #endregion + + #region ToggleInactivated + + [Fact] + public async Task ToggleInactivated_ShouldReturnOk_WhenToggleSucceeds() + { + // Arrange + var categoryId = TestUtils.Guids[0]; + _serviceMock.Setup(s => s.ToggleInactive(categoryId, true)).ReturnsAsync(true); + + // Act + var result = await _controller.ToggleInactivated(categoryId); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task ToggleInactivated_ShouldReturnUnprocessableEntity_WhenToggleFails() + { + // Arrange + var categoryId = TestUtils.Guids[0]; + _serviceMock.Setup(s => s.ToggleInactive(categoryId, true)).ReturnsAsync(false); + + // Act + var result = await _controller.ToggleInactivated(categoryId); + + // Assert + result.Should().BeOfType(); + } + + #endregion + + #region Delete + + [Fact] + public async Task Delete_ShouldReturnOk_WhenDeleteSucceeds() + { + // Arrange + var categoryId = TestUtils.Guids[0]; + _serviceMock.Setup(s => s.Delete(categoryId, true)).ReturnsAsync(true); + + // Act + var result = await _controller.Delete(categoryId); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task Delete_ShouldReturnUnprocessableEntity_WhenDeleteFails() + { + // Arrange + var categoryId = TestUtils.Guids[0]; + _serviceMock.Setup(s => s.Delete(categoryId, true)).ReturnsAsync(false); + + // Act + var result = await _controller.Delete(categoryId); + + // Assert + result.Should().BeOfType(); + } + + #endregion +} \ No newline at end of file diff --git a/Fin.Test/TitleCategories/Services/TitleCategoryServiceTest.cs b/Fin.Test/TitleCategories/Services/TitleCategoryServiceTest.cs new file mode 100644 index 0000000..4082455 --- /dev/null +++ b/Fin.Test/TitleCategories/Services/TitleCategoryServiceTest.cs @@ -0,0 +1,445 @@ +using Fin.Application.TitleCategories; +using Fin.Application.TitleCategories.Dtos; +using Fin.Domain.TitleCategories.Dtos; +using Fin.Domain.TitleCategories.Entities; +using Fin.Infrastructure.Database.Repositories; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; + +namespace Fin.Test.TitleCategories.Services; + +public class TitleCategoryServiceTest : TestUtils.BaseTestWithContext +{ + #region Get + + [Fact] + public async Task Get_ShouldReturnTitleCategory_WhenExists() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + var titleCategory = new TitleCategory(new TitleCategoryInput { Name = TestUtils.Strings[0], Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }); + await resources.TitleCategoryRepository.AddAsync(titleCategory, true); + + // Act + var result = await service.Get(titleCategory.Id); + + // Assert + result.Should().NotBeNull(); + result.Id.Should().Be(titleCategory.Id); + result.Name.Should().Be(titleCategory.Name); + } + + [Fact] + public async Task Get_ShouldReturnNull_WhenNotExists() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + // Act + var result = await service.Get(TestUtils.Guids[9]); + + // Assert + result.Should().BeNull(); + } + + #endregion + + #region GetList + + [Fact] + public async Task GetList_ShouldReturnPagedResult_WithoutFilter() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + await resources.TitleCategoryRepository.AddAsync(new TitleCategory(new TitleCategoryInput { Name = "C", Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }), true); + await resources.TitleCategoryRepository.AddAsync(new TitleCategory(new TitleCategoryInput { Name = "A", Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }), true); + await resources.TitleCategoryRepository.AddAsync(new TitleCategory(new TitleCategoryInput { Name = "B", Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }), true); + + var input = new TitleCategoryGetListInput { MaxResultCount = 2, SkipCount = 0 }; + + // Act + var result = await service.GetList(input); + + // Assert + result.Should().NotBeNull(); + result.TotalCount.Should().Be(3); + result.Items.Should().HaveCount(2); + // Default sort is Inactivated (false first) then Name (asc). All are not inactivated, so sort by Name. + result.Items.First().Name.Should().Be("A"); + result.Items.Last().Name.Should().Be("B"); + } + + [Fact] + public async Task GetList_ShouldFilterByInactivatedTrue() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + await resources.TitleCategoryRepository.AddAsync(new TitleCategory(new TitleCategoryInput { Name = "Active1", Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }), true); + var inactive = new TitleCategory(new TitleCategoryInput { Name = "Inactive1", Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }); + inactive.ToggleInactivated(); + await resources.TitleCategoryRepository.AddAsync(inactive, true); + var inactive2 = new TitleCategory(new TitleCategoryInput { Name = "Inactive2", Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }); + inactive2.ToggleInactivated(); + await resources.TitleCategoryRepository.AddAsync(inactive2, true); + + var input = new TitleCategoryGetListInput { Inactivated = true, MaxResultCount = 10, SkipCount = 0 }; + + // Act + var result = await service.GetList(input); + + // Assert + result.Should().NotBeNull(); + result.TotalCount.Should().Be(2); + result.Items.Should().HaveCount(2); + result.Items.First().Name.Should().Be("Inactive1"); // Sorted by Name asc + result.Items.Last().Name.Should().Be("Inactive2"); + } + + [Fact] + public async Task GetList_ShouldFilterByInactivatedFalse() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + await resources.TitleCategoryRepository.AddAsync(new TitleCategory(new TitleCategoryInput { Name = "Active1", Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }), true); + var inactive = new TitleCategory(new TitleCategoryInput { Name = "Inactive1", Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }); + inactive.ToggleInactivated(); + await resources.TitleCategoryRepository.AddAsync(inactive, true); + await resources.TitleCategoryRepository.AddAsync(new TitleCategory(new TitleCategoryInput { Name = "Active2", Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }), true); + + var input = new TitleCategoryGetListInput { Inactivated = false, MaxResultCount = 10, SkipCount = 0 }; + + // Act + var result = await service.GetList(input); + + // Assert + result.Should().NotBeNull(); + result.TotalCount.Should().Be(2); + result.Items.Should().HaveCount(2); + result.Items.First().Name.Should().Be("Active1"); // Sorted by Name asc + result.Items.Last().Name.Should().Be("Active2"); + } + + #endregion + + #region Create + + [Fact] + public async Task Create() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + DateTimeProvider.Setup(d => d.UtcNow()).Returns(TestUtils.UtcDateTimes[0]); + + var input = new TitleCategoryInput + { + Name = TestUtils.Strings[0], + Color = TestUtils.Strings[1], + Icon = TestUtils.Strings[2] + }; + + // Act + var result = await service.Create(input, true); + + // Assert + result.Should().NotBeNull(); + var dbTitleCategory = await resources.TitleCategoryRepository.Query(false).FirstOrDefaultAsync(a => a.Id == result.Id); + dbTitleCategory.Should().NotBeNull(); + dbTitleCategory.Name.Should().Be(input.Name); + dbTitleCategory.Color.Should().Be(input.Color); + dbTitleCategory.Icon.Should().Be(input.Icon); + dbTitleCategory.Inactivated.Should().BeFalse(); + } + + [Fact] + public async Task Create_NameRequired() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + DateTimeProvider.Setup(d => d.UtcNow()).Returns(TestUtils.UtcDateTimes[0]); + + var input = new TitleCategoryInput + { + Color = TestUtils.Strings[0], + Icon = TestUtils.Strings[0] + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await service.Create(input, true)); + exception.Message.Should().Be("Name is required"); + } + + [Fact] + public async Task Create_ColorRequired() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + DateTimeProvider.Setup(d => d.UtcNow()).Returns(TestUtils.UtcDateTimes[0]); + + var input = new TitleCategoryInput + { + Name = TestUtils.Strings[0], + Icon = TestUtils.Strings[0] + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await service.Create(input, true)); + // Note: The service validates 'Color' but throws an error message 'FrontRoute is required' + exception.Message.Should().Be("FrontRoute is required"); + } + + [Fact] + public async Task Create_IconRequired() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + DateTimeProvider.Setup(d => d.UtcNow()).Returns(TestUtils.UtcDateTimes[0]); + + var input = new TitleCategoryInput + { + Name = TestUtils.Strings[0], + Color = TestUtils.Strings[0] + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await service.Create(input, true)); + exception.Message.Should().Be("Icon is required"); + } + + #endregion + + #region Update + + [Fact] + public async Task Update_ShouldReturnFalse_WhenTitleCategoryNotFound() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + // Act + var result = await service.Update(TestUtils.Guids[9], new TitleCategoryInput{ Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3], Name = TestUtils.Strings[0]}, true); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public async Task Update_ShouldReturnTrue() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + DateTimeProvider.Setup(d => d.UtcNow()).Returns(TestUtils.UtcDateTimes[0]); + var titleCategory = new TitleCategory(new TitleCategoryInput { Name = TestUtils.Strings[0], Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }); + await resources.TitleCategoryRepository.AddAsync(titleCategory, true); + + var input = new TitleCategoryInput { Name = TestUtils.Strings[4], Color = TestUtils.Strings[5], Icon = TestUtils.Strings[6] }; + + // Act + var result = await service.Update(titleCategory.Id, input, true); + + // Assert + result.Should().BeTrue(); + + var dbTitleCategory = await resources.TitleCategoryRepository.Query(false).FirstOrDefaultAsync(a => a.Id == titleCategory.Id); + dbTitleCategory.Should().NotBeNull(); + dbTitleCategory.Name.Should().Be(input.Name); + dbTitleCategory.Color.Should().Be(input.Color); + dbTitleCategory.Icon.Should().Be(input.Icon); + } + + [Fact] + public async Task Update_ShouldThrow_NameRequired() + { + + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + DateTimeProvider.Setup(d => d.UtcNow()).Returns(TestUtils.UtcDateTimes[0]); + var titleCategory = new TitleCategory(new TitleCategoryInput { Name = TestUtils.Strings[0], Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }); + await resources.TitleCategoryRepository.AddAsync(titleCategory, true); + + var input = new TitleCategoryInput { Color = TestUtils.Strings[5], Icon = TestUtils.Strings[6] }; + + // Act & asser + var exception = await Assert.ThrowsAsync(async () => await service.Update(titleCategory.Id, input, true)); + exception.Message.Should().Be("Name is required"); + } + + [Fact] + public async Task Update_ShouldThrow_ColorRequired() + { + + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + DateTimeProvider.Setup(d => d.UtcNow()).Returns(TestUtils.UtcDateTimes[0]); + var titleCategory = new TitleCategory(new TitleCategoryInput { Name = TestUtils.Strings[0], Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }); + await resources.TitleCategoryRepository.AddAsync(titleCategory, true); + + var input = new TitleCategoryInput { Name = TestUtils.Strings[5], Icon = TestUtils.Strings[6] }; + + // Act & asser + var exception = await Assert.ThrowsAsync(async () => await service.Update(titleCategory.Id, input, true)); + exception.Message.Should().Be("FrontRoute is required"); + } + + [Fact] + public async Task Update_ShouldThrow_IconRequired() + { + + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + DateTimeProvider.Setup(d => d.UtcNow()).Returns(TestUtils.UtcDateTimes[0]); + var titleCategory = new TitleCategory(new TitleCategoryInput { Name = TestUtils.Strings[0], Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }); + await resources.TitleCategoryRepository.AddAsync(titleCategory, true); + + var input = new TitleCategoryInput { Name = TestUtils.Strings[5], Color = TestUtils.Strings[6] }; + + // Act & asser + var exception = await Assert.ThrowsAsync(async () => await service.Update(titleCategory.Id, input, true)); + exception.Message.Should().Be("Icon is required"); + } + + #endregion + + #region Delete + + [Fact] + public async Task Delete_ShouldReturnFalse_WhenTitleCategoryNotFound() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + // Act + var result = await service.Delete(TestUtils.Guids[9], true); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public async Task Delete_ShouldReturnTrue() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + DateTimeProvider.Setup(d => d.UtcNow()).Returns(TestUtils.UtcDateTimes[0]); + var titleCategory = new TitleCategory(new TitleCategoryInput { Name = TestUtils.Strings[0], Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }); + await resources.TitleCategoryRepository.AddAsync(titleCategory, true); + + // Act + var result = await service.Delete(titleCategory.Id, true); + + // Assert + result.Should().BeTrue(); + (await resources.TitleCategoryRepository.Query(false).FirstOrDefaultAsync(a => a.Id == titleCategory.Id)).Should().BeNull(); + } + + #endregion + + #region ToggleInactive + + [Fact] + public async Task ToggleInactive_ShouldReturnFalse_WhenTitleCategoryNotFound() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + // Act + var result = await service.ToggleInactive(TestUtils.Guids[9], true); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public async Task ToggleInactive_ShouldDeactivate() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + DateTimeProvider.Setup(d => d.UtcNow()).Returns(TestUtils.UtcDateTimes[0]); + var titleCategory = new TitleCategory(new TitleCategoryInput { Name = TestUtils.Strings[0], Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }); + await resources.TitleCategoryRepository.AddAsync(titleCategory, true); + titleCategory.Inactivated.Should().BeFalse(); + + // Act + var result = await service.ToggleInactive(titleCategory.Id, true); + + // Assert + result.Should().BeTrue(); + var dbTitleCategory = await resources.TitleCategoryRepository.Query(false).FirstOrDefaultAsync(a => a.Id == titleCategory.Id); + dbTitleCategory.Should().NotBeNull(); + dbTitleCategory.Inactivated.Should().BeTrue(); + } + + [Fact] + public async Task ToggleInactive_ShouldReactivate() + { + // Arrange + var resources = GetResources(); + var service = GetService(resources); + + DateTimeProvider.Setup(d => d.UtcNow()).Returns(TestUtils.UtcDateTimes[0]); + var titleCategory = new TitleCategory(new TitleCategoryInput { Name = TestUtils.Strings[0], Color = TestUtils.Strings[1], Icon = TestUtils.Strings[3] }); + titleCategory.ToggleInactivated(); + await resources.TitleCategoryRepository.AddAsync(titleCategory, true); + titleCategory.Inactivated.Should().BeTrue(); + + // Act + var result = await service.ToggleInactive(titleCategory.Id, true); + + // Assert + result.Should().BeTrue(); + var dbTitleCategory = await resources.TitleCategoryRepository.Query(false).FirstOrDefaultAsync(a => a.Id == titleCategory.Id); + dbTitleCategory.Should().NotBeNull(); + dbTitleCategory.Inactivated.Should().BeFalse(); + } + + #endregion + + private TitleCategoryService GetService(Resources resources) + { + return new TitleCategoryService(resources.TitleCategoryRepository); + } + + private Resources GetResources() + { + return new Resources + { + TitleCategoryRepository = GetRepository() + }; + } + + private class Resources + { + public IRepository TitleCategoryRepository { get; set; } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7f54fc6..baa4cf6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,7 @@ services: volumes: - redis_data:/data healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] interval: 10s timeout: 3s retries: 5