using System.Runtime.CompilerServices;
using BuildFlow.Shared.Domain.Errors;
using BuildFlow.Shared.Domain.Exceptions;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Monitoring.Application.ExternalSystems.Commands;
using Monitoring.Application.ExternalSystems.Handlers;
using Monitoring.Application.Reports.Handlers;
using Monitoring.Application.Reports.Queries;
using Monitoring.Domain.Entities.SyncedRecord;
using Monitoring.Domain.Entities.SyncJob;
using Monitoring.Domain.Enums;
using Monitoring.Domain.Values;
using Monitoring.Infrastructure.Persistence;
namespace BuildFlow.Monitoring.Tests;
///
/// [SOP-MONITORING] Reports 子域 Handler 测试(GetDailyTrend / GetSyncSummary)。
/// 两者均为纯 DbContext 查询,不依赖 SyncProcessor/外部连接器,可用 InMemory 直接覆盖。
///
public class ReportsHandlerTests
{
private static MonitoringDbContext Db([CallerMemberName] string name = "")
=> MonitoringTestDb.Create(name);
private static async Task SeedSystem(MonitoringDbContext db, string name = "u号租")
{
var created = await new CreateExternalSystemCommandHandler(db).Handle(
new CreateExternalSystemCommand(name, 1, "https://x", 60, null),
CancellationToken.None);
return created.Id;
}
#region GetDailyTrend
[Fact]
public async Task GetDailyTrend_Empty_ReturnsEmpty()
{
await using var db = Db();
var sysId = await SeedSystem(db);
var result = await new GetDailyTrendQueryHandler(db)
.Handle(new GetDailyTrendQuery(sysId, 14), CancellationToken.None);
result.Should().BeEmpty();
}
[Fact]
public async Task GetDailyTrend_GroupsByDay()
{
await using var db = Db();
var sysId = await SeedSystem(db);
var today = DateTime.UtcNow.Date;
db.SyncedRecords.Add(new SyncedRecordEntity
{
ExternalSystemId = sysId, SyncJobId = Guid.NewGuid(), ExternalId = "o1",
RecordType = "order", OccurredAt = today.AddHours(1), JsonData = "{}"
});
db.SyncedRecords.Add(new SyncedRecordEntity
{
ExternalSystemId = sysId, SyncJobId = Guid.NewGuid(), ExternalId = "o2",
RecordType = "order", OccurredAt = today.AddHours(2), JsonData = "{}"
});
db.SyncedRecords.Add(new SyncedRecordEntity
{
ExternalSystemId = sysId, SyncJobId = Guid.NewGuid(), ExternalId = "o3",
RecordType = "order", OccurredAt = today.AddDays(-2), JsonData = "{}"
});
await db.SaveChangesAsync();
var result = await new GetDailyTrendQueryHandler(db)
.Handle(new GetDailyTrendQuery(sysId, 14), CancellationToken.None);
result.Should().HaveCount(2, "今天 2 条 + 2 天前 1 条,跨 2 个日期");
result.Sum(r => r.RecordCount).Should().Be(3);
}
#endregion
#region GetSyncSummary
[Fact]
public async Task GetSyncSummary_Existing_ReturnsSummary()
{
await using var db = Db();
var sysId = await SeedSystem(db);
db.SyncJobs.Add(new SyncJobEntity
{
ExternalSystemId = sysId,
Status = SyncJobStatusEnum.Succeeded,
StartedAt = DateTime.UtcNow.AddMinutes(-5),
FinishedAt = DateTime.UtcNow,
RecordCount = 100,
DurationMs = 1234,
Data = new JobData { TriggerSource = "manual", SummaryMetrics = new() { ["order_count"] = "100" } }
});
await db.SaveChangesAsync();
var result = await new GetSyncSummaryQueryHandler(db)
.Handle(new GetSyncSummaryQuery(sysId, 10), CancellationToken.None);
result.Should().NotBeNull();
result.ExternalSystemId.Should().Be(sysId);
result.SystemName.Should().Be("u号租");
result.RecentJobs.Should().HaveCount(1);
result.RecentJobs[0].Status.Should().Be("Succeeded");
result.RecentJobs[0].RecordCount.Should().Be(100);
result.LatestMetrics.Should().ContainKey("order_count");
}
[Fact]
public async Task GetSyncSummary_NotFound_Throws()
{
await using var db = Db();
var act = async () => await new GetSyncSummaryQueryHandler(db)
.Handle(new GetSyncSummaryQuery(Guid.NewGuid(), 10), CancellationToken.None);
var ex = await act.Should().ThrowAsync();
ex.Which.Code.Should().Be(ErrorCode.NotFound);
}
#endregion
}