using BuildFlow.Shared.Domain.Errors; using BuildFlow.Shared.Domain.Exceptions; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Monitoring.Application.ExternalSystems.Commands; using Monitoring.Application.ExternalSystems.DTOs; using Monitoring.Application.ExternalSystems.Handlers; using Monitoring.Application.ExternalSystems.Queries; using Monitoring.Infrastructure.Persistence; namespace BuildFlow.Monitoring.Tests; /// /// [SOP-MONITORING] ExternalSystems 子域 Handler 全覆盖测试。 /// 覆盖: Create / List / GetDetail / Update(含无效类型、缺失、非法 JSON、凭证覆盖分支)。 /// 约定: 直接用 new Handler(db) 调 Handle;db 由 MonitoringTestDb.Create 提供(自动补审计字段)。 /// 用 Create handler 串联构造前置数据,避免直接依赖实体内部构造。 /// public class ExternalSystemsHandlerTests { private static MonitoringDbContext Db([System.Runtime.CompilerServices.CallerMemberName] string name = "") => MonitoringTestDb.Create(name); #region Create [Fact] public async Task Create_Valid_ReturnsDtoAndPersists() { await using var db = Db(); var result = await new CreateExternalSystemCommandHandler(db).Handle( new CreateExternalSystemCommand("u号租", 1, "https://api.example.com", 300, null), CancellationToken.None); result.Should().NotBeNull(); result.Name.Should().Be("u号租"); result.SystemType.Should().Be(1); result.BaseUrl.Should().Be("https://api.example.com"); result.IsEnabled.Should().BeTrue(); result.PollIntervalSeconds.Should().Be(300); (await db.ExternalSystems.CountAsync()).Should().Be(1); } [Fact] public async Task Create_InvalidSystemType_ThrowsBusiness() { await using var db = Db(); var act = async () => await new CreateExternalSystemCommandHandler(db).Handle( new CreateExternalSystemCommand("x", 999, "https://x", 0, null), CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.InvalidParam); } #endregion #region List [Fact] public async Task List_Empty_ReturnsEmpty() { await using var db = Db(); var result = await new ListExternalSystemsQueryHandler(db) .Handle(new ListExternalSystemsQuery(), CancellationToken.None); result.Should().BeEmpty(); } [Fact] public async Task List_ReturnsAllDescendingByCreatedAt() { await using var db = Db(); var create = new CreateExternalSystemCommandHandler(db); var a = await create.Handle(new CreateExternalSystemCommand("A", 1, "https://a", 1, null), CancellationToken.None); var b = await create.Handle(new CreateExternalSystemCommand("B", 1, "https://b", 1, null), CancellationToken.None); // 强制 A 更早创建,使倒序断言稳定 var entA = await db.ExternalSystems.FirstAsync(x => x.Id == a.Id); entA.CreatedAt = DateTime.UtcNow.AddMinutes(-10); await db.SaveChangesAsync(); var list = await new ListExternalSystemsQueryHandler(db) .Handle(new ListExternalSystemsQuery(), CancellationToken.None); list.Should().HaveCount(2); list[0].Name.Should().Be("B"); list[1].Name.Should().Be("A"); } #endregion #region GetDetail [Fact] public async Task GetDetail_Existing_ReturnsDetail() { await using var db = Db(); var created = await new CreateExternalSystemCommandHandler(db).Handle( new CreateExternalSystemCommand("u号租", 1, "https://x", 60, null), CancellationToken.None); var detail = await new GetExternalSystemDetailQueryHandler(db) .Handle(new GetExternalSystemDetailQuery(created.Id), CancellationToken.None); detail.Should().NotBeNull(); detail.Id.Should().Be(created.Id); detail.Name.Should().Be("u号租"); } [Fact] public async Task GetDetail_NotFound_Throws() { await using var db = Db(); var act = async () => await new GetExternalSystemDetailQueryHandler(db) .Handle(new GetExternalSystemDetailQuery(Guid.NewGuid()), CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.NotFound); } #endregion #region Update [Fact] public async Task Update_NameAndBaseUrl_Overwrites() { await using var db = Db(); var created = await new CreateExternalSystemCommandHandler(db).Handle( new CreateExternalSystemCommand("old", 1, "https://old", 60, null), CancellationToken.None); var result = await new UpdateExternalSystemCommandHandler(db).Handle( new UpdateExternalSystemCommand(created.Id, "new", "https://new", null, null, null, null), CancellationToken.None); result.Name.Should().Be("new"); result.BaseUrl.Should().Be("https://new"); } [Fact] public async Task Update_NonExistent_ThrowsNotFound() { await using var db = Db(); var act = async () => await new UpdateExternalSystemCommandHandler(db).Handle( new UpdateExternalSystemCommand(Guid.NewGuid(), "x", null, null, null, null, null), CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.NotFound); } [Fact] public async Task Update_InvalidParametersJson_ThrowsBusiness() { await using var db = Db(); var created = await new CreateExternalSystemCommandHandler(db).Handle( new CreateExternalSystemCommand("x", 1, "https://x", 60, null), CancellationToken.None); var act = async () => await new UpdateExternalSystemCommandHandler(db).Handle( new UpdateExternalSystemCommand(created.Id, null, null, null, null, "{bad json", null), CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.InvalidParam); } [Fact] public async Task Update_Credentials_Overwrites() { await using var db = Db(); var created = await new CreateExternalSystemCommandHandler(db).Handle( new CreateExternalSystemCommand("x", 1, "https://x", 60, null), CancellationToken.None); var creds = new List { new("token", "ak", "av", true, null) }; await new UpdateExternalSystemCommandHandler(db).Handle( new UpdateExternalSystemCommand(created.Id, null, null, null, null, null, creds), CancellationToken.None); var detail = await new GetExternalSystemDetailQueryHandler(db) .Handle(new GetExternalSystemDetailQuery(created.Id), CancellationToken.None); detail.Credentials.Should().HaveCount(1); detail.Credentials[0].Key.Should().Be("ak"); } #endregion }