Files
live_recorder/tests/LiveRecorder.Tests/RecoveryServiceTests.cs
T

171 lines
6.6 KiB
C#

using LiveRecorder.Application.Abstractions.Logging;
using LiveRecorder.Application.Abstractions.Recording;
using LiveRecorder.Application.Models.Logs;
using LiveRecorder.Domain.Entities;
using LiveRecorder.Domain.Enums;
using LiveRecorder.Infrastructure.Persistence;
using LiveRecorder.Infrastructure.Services;
using Microsoft.EntityFrameworkCore;
namespace LiveRecorder.Tests;
public sealed class RecoveryServiceTests : IDisposable
{
private readonly string _temporaryDirectory = Path.Combine(
Path.GetTempPath(),
$"liverecorder-artifact-recovery-{Guid.NewGuid():N}");
[Fact]
public async Task AcceptShortArtifact_RequiresConfirmationThenLeavesItForManualUpload()
{
Directory.CreateDirectory(_temporaryDirectory);
var mediaPath = Path.Combine(_temporaryDirectory, "short-fragment.mp4");
await File.WriteAllBytesAsync(mediaPath, [1, 2, 3, 4]);
await using var context = CreateContext();
var taskId = await SeedFailedTaskAsync(context, mediaPath, durationSeconds: 3);
var service = CreateService(context, new VideoMetadata(3, 1920, 1080, "h264", "aac", 30, 2_000_000));
var rejected = await service.AcceptRecordingArtifactAsync(taskId, confirmShortArtifact: false);
Assert.Equal(0, rejected.SuccessCount);
Assert.Contains("需要确认短分片", rejected.Messages.Single());
Assert.Equal(RecordTaskStatus.Failed, (await context.RecordTasks.FindAsync(taskId))!.Status);
var accepted = await service.AcceptRecordingArtifactAsync(taskId, confirmShortArtifact: true);
Assert.Equal(1, accepted.SuccessCount);
var task = await context.RecordTasks.Include(item => item.Result).SingleAsync(item => item.Id == taskId);
Assert.Equal(RecordTaskStatus.Completed, task.Status);
Assert.Equal(RecordArtifactUploadStatus.NotUploaded, task.Result!.UploadStatus);
Assert.Null(task.Result.LastUploadProvider);
Assert.Null(task.Result.LastUploadedAt);
Assert.Null(task.Result.UploadErrorMessage);
Assert.True(File.Exists(mediaPath));
}
[Fact]
public async Task ListFailures_IdentifiesMissingMediaAsUnrecoverable()
{
Directory.CreateDirectory(_temporaryDirectory);
var missingPath = Path.Combine(_temporaryDirectory, "missing.mp4");
await using var context = CreateContext();
await SeedFailedTaskAsync(context, missingPath, durationSeconds: 120);
var service = CreateService(context, metadata: null);
var response = await service.ListRecordingFailuresAsync("MissingMedia", 0, 20);
var item = Assert.Single(response.Items);
Assert.Equal("MissingMedia", item.FailureKind);
Assert.False(item.FileExists);
Assert.False(item.CanAccept);
Assert.False(item.CanRepair);
}
private static LiveRecorderDbContext CreateContext()
{
var options = new DbContextOptionsBuilder<LiveRecorderDbContext>()
.UseInMemoryDatabase($"artifact-recovery-{Guid.NewGuid():N}")
.Options;
return new LiveRecorderDbContext(options);
}
private static async Task<Guid> SeedFailedTaskAsync(
LiveRecorderDbContext context,
string mediaPath,
double durationSeconds)
{
var now = DateTimeOffset.UtcNow;
var room = new LiveRoom(
LivePlatformType.Douyin,
"https://live.example/recovery",
"recovery-room",
"https://live.example/recovery",
now.AddHours(-1));
room.UpdateMetadata("恢复测试直播间", "恢复主播", null, null, null, now.AddHours(-1));
var session = new RecordSession(
room.Id,
"origin",
RecordOutputFormat.Mp4,
RecordSaveMode.Segmented,
now.AddMinutes(-10));
session.MarkFailed("ffmpeg exited unexpectedly", now);
var task = new RecordTask(
room.Id,
session.Id,
1,
"origin",
RecordOutputFormat.Mp4,
now.AddMinutes(-10));
task.MarkStarting("https://stream.example/recovery", mediaPath, now.AddMinutes(-10));
task.MarkFailed("ffmpeg exited unexpectedly", now, durationSeconds);
var result = new RecordResult(
task.Id,
mediaPath,
File.Exists(mediaPath) ? new FileInfo(mediaPath).Length : null,
durationSeconds,
null,
0,
RecordTaskStatus.Failed,
task.ErrorMessage,
now);
result.MarkUploadFailed("openlist", "previous upload error", now);
context.AddRange(room, session, task, result);
await context.SaveChangesAsync();
return task.Id;
}
private static RecoveryService CreateService(LiveRecorderDbContext context, VideoMetadata? metadata) =>
new(
context,
systemSettingsService: null!,
storageGuardService: null!,
ffmpegService: null!,
recordService: null!,
videoMetadataService: new FixedVideoMetadataService(metadata),
systemLogService: new NullSystemLogService());
public void Dispose()
{
if (Directory.Exists(_temporaryDirectory))
{
Directory.Delete(_temporaryDirectory, recursive: true);
}
}
private sealed class FixedVideoMetadataService(VideoMetadata? metadata) : IVideoMetadataService
{
public Task<VideoMetadata?> ExtractMetadataAsync(
string filePath,
CancellationToken cancellationToken = default) => Task.FromResult(metadata);
public Task<string?> GenerateThumbnailAsync(
string filePath,
string outputDir,
CancellationToken cancellationToken = default) => Task.FromResult<string?>(null);
}
private sealed class NullSystemLogService : ISystemLogService
{
public Task WriteAsync(
SystemLogLevel level,
string category,
string message,
string? detail = null,
Guid? liveRoomId = null,
Guid? recordSessionId = null,
Guid? recordTaskId = null,
CancellationToken cancellationToken = default) => 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>>([]);
}
}