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
}