171 lines
6.6 KiB
C#
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>>([]);
|
|
}
|
|
}
|