fix: harden auto uploads and table layouts

This commit is contained in:
2026-09-19 10:25:10 +08:00
parent f0fb4db755
commit 8960b68ac8
15 changed files with 586 additions and 48 deletions
@@ -4,6 +4,7 @@ using System.Text;
using System.Text.Json;
using LiveRecorder.Application.Abstractions.Logging;
using LiveRecorder.Application.Abstractions.Recording;
using LiveRecorder.Application.Abstractions.Scripting;
using LiveRecorder.Application.Abstractions.Settings;
using LiveRecorder.Application.Models.Logs;
using LiveRecorder.Application.Models.Settings;
@@ -14,11 +15,83 @@ using LiveRecorder.Infrastructure.Persistence.Repositories;
using LiveRecorder.Infrastructure.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging.Abstractions;
namespace LiveRecorder.Tests;
public sealed class OpenListUploadTests
{
[Fact]
public async Task TestConnection_ReportsDestinationMountFailureAfterSuccessfulLogin()
{
var handler = new StubHttpMessageHandler(async request =>
{
var path = request.RequestUri!.AbsolutePath;
if (path.EndsWith("/api/auth/login", StringComparison.Ordinal))
{
return JsonResponse("""{"code":200,"message":"success","data":{"token":"test-token"}}""");
}
if (path.EndsWith("/api/public/settings", StringComparison.Ordinal))
{
return JsonResponse("""{"code":200,"message":"success","data":{"version":"4.1.10"}}""");
}
var body = JsonDocument.Parse(await request.Content!.ReadAsStringAsync()).RootElement;
return body.GetProperty("path").GetString() == "/source"
? JsonResponse("""{"code":200,"message":"success","data":{"content":[],"write":false}}""")
: JsonResponse("""{"code":500,"message":"provider timeout","data":null}""");
});
var client = CreateClient(handler);
var connection = Connection();
connection.SourcePath = "/source";
connection.DestinationPath = "/destination";
var result = await client.TestConnectionAsync(connection);
Assert.False(result.Success);
Assert.Equal("4.1.10", result.Version);
Assert.True(result.SourcePath?.Success);
Assert.False(result.DestinationPath?.Success);
Assert.Contains("provider timeout", result.DestinationPath?.Message);
}
[Fact]
public async Task CompletionDispatch_ScriptFailureDoesNotBlockAutomaticUpload()
{
await using var fixture = await QueueFixture.CreateAsync();
await fixture.PrepareCompletionDispatchAsync();
var service = fixture.CreateCompletionDispatchService(new FixedEventScriptService(
new EventScriptExecutionResultDto { Success = false, Message = "script failed" }));
await service.TryDispatchTaskAsync(fixture.RecordTaskId);
var dispatch = await fixture.Context.RecordCompletionDispatches.SingleAsync();
Assert.False(dispatch.ScriptDispatched);
Assert.True(dispatch.UploadDispatched);
Assert.Null(dispatch.CompletedAt);
Assert.Contains("script failed", dispatch.LastError);
Assert.NotNull(await fixture.Context.RecordUploadJobs.SingleOrDefaultAsync());
}
[Fact]
public async Task CompletionDispatch_UploadRejectionRemainsRetryable()
{
await using var fixture = await QueueFixture.CreateAsync(
new VideoMetadata(0.18, 1920, 1080, "h264", "aac", 30, 4_000_000));
await fixture.PrepareCompletionDispatchAsync();
var service = fixture.CreateCompletionDispatchService(new FixedEventScriptService(null));
await service.TryDispatchTaskAsync(fixture.RecordTaskId);
var dispatch = await fixture.Context.RecordCompletionDispatches.SingleAsync();
Assert.True(dispatch.ScriptDispatched);
Assert.False(dispatch.UploadDispatched);
Assert.Null(dispatch.CompletedAt);
Assert.Contains("自动上传", dispatch.LastError);
Assert.True(dispatch.NextAttemptAt > DateTimeOffset.UtcNow);
}
[Fact]
public async Task Enqueue_RejectsShortMediaAndKeepsItNotUploaded()
{
@@ -763,6 +836,30 @@ public sealed class OpenListUploadTests
public Guid RecordTaskId { get; }
public async Task PrepareCompletionDispatchAsync()
{
var session = await Context.RecordSessions.SingleAsync();
session.MarkCompleted(DateTimeOffset.UtcNow);
if (!await Context.RecordCompletionDispatches.AnyAsync())
{
Context.RecordCompletionDispatches.Add(
new RecordCompletionDispatch(RecordTaskId, DateTimeOffset.UtcNow));
}
await Context.SaveChangesAsync();
}
public CompletionDispatchService CreateCompletionDispatchService(IEventScriptService eventScriptService)
{
var uploadService = new RecordUploadService(Context, _settingsService, new NullSystemLogService(), Queue);
var consolidation = new ShortFragmentConsolidationService(
Context,
_settingsService,
_videoMetadataService,
NullLogger<ShortFragmentConsolidationService>.Instance);
return new CompletionDispatchService(Context, eventScriptService, uploadService, consolidation);
}
public static async Task<QueueFixture> CreateAsync(VideoMetadata? metadata = null)
{
var temporaryRoot = Path.Combine(Path.GetTempPath(), $"live-recorder-openlist-{Guid.NewGuid():N}");
@@ -886,6 +983,42 @@ public sealed class OpenListUploadTests
CancellationToken cancellationToken = default) => Task.FromResult<string?>(null);
}
private sealed class FixedEventScriptService : IEventScriptService
{
private readonly EventScriptExecutionResultDto? _segmentResult;
public FixedEventScriptService(EventScriptExecutionResultDto? segmentResult)
{
_segmentResult = segmentResult;
}
public Task<EventScriptExecutionResultDto?> RunLiveStartedAsync(
LiveRoom liveRoom,
DateTimeOffset occurredAt,
CancellationToken cancellationToken = default) => Task.FromResult<EventScriptExecutionResultDto?>(null);
public Task<EventScriptExecutionResultDto?> RunLiveEndedAsync(
LiveRoom liveRoom,
DateTimeOffset occurredAt,
CancellationToken cancellationToken = default) => Task.FromResult<EventScriptExecutionResultDto?>(null);
public Task<EventScriptExecutionResultDto?> RunSegmentCompletedAsync(
LiveRoom? liveRoom,
RecordSession recordSession,
RecordTask recordTask,
RecordResult? recordResult,
string segmentFilePath,
DateTimeOffset occurredAt,
bool forceRun = false,
Guid? eventId = null,
CancellationToken cancellationToken = default) => Task.FromResult(_segmentResult);
public Task<EventScriptTestResultDto> TestAsync(
TestEventScriptRequest request,
CancellationToken cancellationToken = default) =>
Task.FromResult(new EventScriptTestResultDto { Success = true, Message = "ok" });
}
private sealed class FixedSettingsService : ISystemSettingsService
{
private readonly SystemSettingsDto _settings;