fix: harden recording recovery and dashboard metrics
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
using LiveRecorder.Application.Abstractions.Logging;
|
||||
using LiveRecorder.Application.Models.Logs;
|
||||
using LiveRecorder.Domain.Entities;
|
||||
using LiveRecorder.Domain.Enums;
|
||||
using LiveRecorder.Infrastructure.Persistence;
|
||||
using LiveRecorder.Infrastructure.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace LiveRecorder.Tests;
|
||||
|
||||
public sealed class StalePendingRecoveryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StalePendingSessionWithoutMedia_IsFailedAndRetainedForAudit()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var databaseRoot = new InMemoryDatabaseRoot();
|
||||
var databaseName = $"stale-pending-{Guid.NewGuid():N}";
|
||||
services.AddDbContext<LiveRecorderDbContext>(options =>
|
||||
options.UseInMemoryDatabase(databaseName, databaseRoot));
|
||||
var logService = new CapturingSystemLogService();
|
||||
services.AddSingleton<ISystemLogService>(logService);
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
|
||||
var createdAt = DateTimeOffset.UtcNow.AddMinutes(-11);
|
||||
Guid sessionId;
|
||||
Guid taskId;
|
||||
await using (var scope = provider.CreateAsyncScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<LiveRecorderDbContext>();
|
||||
var liveRoom = new LiveRoom(
|
||||
LivePlatformType.Douyin,
|
||||
"https://live.example/room",
|
||||
"room-1",
|
||||
"https://live.example/room",
|
||||
createdAt);
|
||||
var session = new RecordSession(
|
||||
liveRoom.Id,
|
||||
"origin",
|
||||
RecordOutputFormat.Mp4,
|
||||
RecordSaveMode.Segmented,
|
||||
createdAt);
|
||||
var task = new RecordTask(
|
||||
liveRoom.Id,
|
||||
session.Id,
|
||||
1,
|
||||
"origin",
|
||||
RecordOutputFormat.Mp4,
|
||||
createdAt);
|
||||
sessionId = session.Id;
|
||||
taskId = task.Id;
|
||||
dbContext.AddRange(liveRoom, session, task);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = new FfmpegService(
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
null!,
|
||||
null!,
|
||||
NullLogger<FfmpegService>.Instance);
|
||||
|
||||
await using (var preconditionScope = provider.CreateAsyncScope())
|
||||
{
|
||||
var preconditionContext = preconditionScope.ServiceProvider.GetRequiredService<LiveRecorderDbContext>();
|
||||
var persisted = await preconditionContext.RecordSessions.SingleAsync();
|
||||
Assert.Equal(RecordSessionStatus.Pending, persisted.Status);
|
||||
Assert.True(
|
||||
persisted.UpdatedAt <= DateTimeOffset.UtcNow.AddMinutes(-10),
|
||||
$"updatedAt={persisted.UpdatedAt:O}");
|
||||
}
|
||||
|
||||
var recovered = await service.RecoverStalePendingSessionsAsync();
|
||||
|
||||
Assert.Equal(1, recovered);
|
||||
await using var verificationScope = provider.CreateAsyncScope();
|
||||
var verificationContext = verificationScope.ServiceProvider.GetRequiredService<LiveRecorderDbContext>();
|
||||
var persistedSession = await verificationContext.RecordSessions.SingleAsync(item => item.Id == sessionId);
|
||||
var persistedTask = await verificationContext.RecordTasks
|
||||
.Include(item => item.Result)
|
||||
.SingleAsync(item => item.Id == taskId);
|
||||
Assert.Equal(RecordSessionStatus.Failed, persistedSession.Status);
|
||||
Assert.Equal(RecordTaskStatus.Failed, persistedTask.Status);
|
||||
Assert.Contains("interrupted before stream initialization", persistedSession.ErrorMessage);
|
||||
Assert.Null(persistedTask.Result);
|
||||
Assert.Single(logService.Entries);
|
||||
Assert.Equal(SystemLogLevel.Warning, logService.Entries[0].Level);
|
||||
}
|
||||
|
||||
private sealed class CapturingSystemLogService : ISystemLogService
|
||||
{
|
||||
public List<(SystemLogLevel Level, string Message)> Entries { get; } = [];
|
||||
|
||||
public Task WriteAsync(
|
||||
SystemLogLevel level,
|
||||
string category,
|
||||
string message,
|
||||
string? detail = null,
|
||||
Guid? liveRoomId = null,
|
||||
Guid? recordSessionId = null,
|
||||
Guid? recordTaskId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Entries.Add((level, message));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<SystemLogDto>> ListAsync(
|
||||
Guid? liveRoomId = null,
|
||||
Guid? recordSessionId = null,
|
||||
Guid? recordTaskId = null,
|
||||
SystemLogLevel? level = null,
|
||||
string? content = null,
|
||||
int take = 200,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult<IReadOnlyList<SystemLogDto>>([]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user