using BuildFlow.Shared.Domain.Errors; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Workflow.Application.Form.ComponentRegistry.Commands; using Workflow.Application.Form.ComponentRegistry.Handlers; using Workflow.Application.Form.ComponentRegistry.Queries; using Workflow.Domain.Entities; using Xunit; namespace BuildFlow.Workflow.Tests.Form; public class FormComponentHandlerTests { private static WorkflowDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var db = new WorkflowDbContext(options); db.SavingChanges += AuditStub.OnSave; return db; } private static FormComponentRegistry SeedComponent(WorkflowDbContext db, string name, string category) { var entity = new FormComponentRegistry { Id = Guid.NewGuid(), Name = name, DisplayName = name, Category = category, Icon = "icon", DefaultSchema = "{}", SupportedProps = "{}" }; db.FormComponentRegistries.Add(entity); return entity; } #region CreateFormComponent [Fact] public async Task CreateFormComponent_SavesAndReturnsDto() { await using var db = CreateDbContext(); var handler = new CreateFormComponentCommandHandler(db); var command = new CreateFormComponentCommand( Name: "input", DisplayName: "Input", Category: "basic", Icon: "i", DefaultSchema: "{}", SupportedProps: "{}"); var result = await handler.Handle(command, CancellationToken.None); result.Should().NotBeNull(); result.Name.Should().Be("input"); result.Category.Should().Be("basic"); (await db.FormComponentRegistries.CountAsync()).Should().Be(1); } [Fact] public async Task CreateFormComponent_DuplicateName_ThrowsAlreadyExists() { await using var db = CreateDbContext(); SeedComponent(db, "input", "basic"); await db.SaveChangesAsync(); var handler = new CreateFormComponentCommandHandler(db); var command = new CreateFormComponentCommand( Name: "input", DisplayName: "Input", Category: "basic", Icon: "i", DefaultSchema: "{}", SupportedProps: "{}"); var act = async () => await handler.Handle(command, CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.AlreadyExists); } #endregion #region UpdateFormComponent [Fact] public async Task UpdateFormComponent_UpdatesFields() { await using var db = CreateDbContext(); var entity = SeedComponent(db, "input", "basic"); await db.SaveChangesAsync(); var handler = new UpdateFormComponentCommandHandler(db); var command = new UpdateFormComponentCommand( Id: entity.Id, DisplayName: "Renamed", Category: "advanced", Icon: "x", DefaultSchema: "{\"a\":1}", SupportedProps: "{\"b\":2}", IsActive: false); var result = await handler.Handle(command, CancellationToken.None); result.DisplayName.Should().Be("Renamed"); result.IsActive.Should().BeFalse(); var stored = await db.FormComponentRegistries.FindAsync(entity.Id); stored!.Category.Should().Be("advanced"); } [Fact] public async Task UpdateFormComponent_Missing_ThrowsNotFound() { await using var db = CreateDbContext(); var handler = new UpdateFormComponentCommandHandler(db); var command = new UpdateFormComponentCommand( Id: Guid.NewGuid(), DisplayName: "Renamed", Category: "advanced", Icon: "x", DefaultSchema: "{}", SupportedProps: "{}", IsActive: true); var act = async () => await handler.Handle(command, CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.NotFound); } #endregion #region DeleteFormComponent [Fact] public async Task DeleteFormComponent_SoftDeletes() { await using var db = CreateDbContext(); var entity = SeedComponent(db, "input", "basic"); await db.SaveChangesAsync(); var handler = new DeleteFormComponentCommandHandler(db); await handler.Handle(new DeleteFormComponentCommand(entity.Id), CancellationToken.None); var stored = await db.FormComponentRegistries.FindAsync(entity.Id); stored!.IsDeleted.Should().BeTrue(); } [Fact] public async Task DeleteFormComponent_Missing_ThrowsNotFound() { await using var db = CreateDbContext(); var handler = new DeleteFormComponentCommandHandler(db); var act = async () => await handler.Handle(new DeleteFormComponentCommand(Guid.NewGuid()), CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.NotFound); } #endregion #region GetFormComponentById [Fact] public async Task GetFormComponentById_ReturnsDto() { await using var db = CreateDbContext(); var entity = SeedComponent(db, "input", "basic"); await db.SaveChangesAsync(); var handler = new GetFormComponentByIdQueryHandler(db); var result = await handler.Handle(new GetFormComponentByIdQuery(entity.Id), CancellationToken.None); result.Should().NotBeNull(); result.Id.Should().Be(entity.Id); result.Name.Should().Be("input"); } [Fact] public async Task GetFormComponentById_Missing_ThrowsNotFound() { await using var db = CreateDbContext(); var handler = new GetFormComponentByIdQueryHandler(db); var act = async () => await handler.Handle(new GetFormComponentByIdQuery(Guid.NewGuid()), CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.NotFound); } #endregion #region GetFormComponents [Fact] public async Task GetFormComponents_FiltersByCategory() { await using var db = CreateDbContext(); SeedComponent(db, "a", "basic"); SeedComponent(db, "b", "advanced"); await db.SaveChangesAsync(); var handler = new GetFormComponentsQueryHandler(db); var result = await handler.Handle(new GetFormComponentsQuery(Category: "basic", IsActive: null), CancellationToken.None); result.Should().HaveCount(1); result[0].Name.Should().Be("a"); } [Fact] public async Task GetFormComponents_Empty_WhenNoMatch() { await using var db = CreateDbContext(); SeedComponent(db, "a", "basic"); await db.SaveChangesAsync(); var handler = new GetFormComponentsQueryHandler(db); var result = await handler.Handle(new GetFormComponentsQuery(Category: "missing", IsActive: null), CancellationToken.None); result.Should().BeEmpty(); } #endregion }