using LiveRecorder.Application.Abstractions.Storage; using LiveRecorder.Application.Models.Settings; using LiveRecorder.Infrastructure.Services; using Microsoft.Extensions.Logging.Abstractions; namespace LiveRecorder.Tests; public sealed class StorageGuardServiceTests { private readonly StorageGuardService _service = new(NullLogger.Instance); [Fact] public void DisabledProtection_StillReportsActualCapacityWithoutBlockingRecording() { var settings = CreateSettings(enableStorageGuard: false); var result = _service.CheckCanStartOrResume(settings); Assert.False(result.IsEnabled); Assert.True(result.IsAvailable); Assert.True(result.HasEnoughSpace); Assert.True(result.CanStartNewRecording); Assert.False(result.ShouldPauseActive); Assert.True(result.TotalBytes > 0); Assert.Equal(result.TotalBytes, result.UsedBytes + result.AvailableBytes); Assert.InRange(result.UsagePercent, 0, 100); Assert.InRange(result.FreePercent, 0, 100); Assert.InRange(result.UsagePercent + result.FreePercent, 99.8, 100.2); } [Fact] public void EnabledProtection_ReportsConfiguredThresholdsAndConsistentPercentages() { var settings = CreateSettings(enableStorageGuard: true); settings.StorageGreenThresholdPercent = 35; settings.StorageRedThresholdPercent = 12; var result = _service.CheckCanStartOrResume(settings); Assert.True(result.IsEnabled); Assert.True(result.IsAvailable); Assert.Equal(35, result.GreenThresholdPercent); Assert.Equal(12, result.RedThresholdPercent); Assert.Equal(result.TotalBytes, result.UsedBytes + result.AvailableBytes); Assert.InRange(result.UsagePercent + result.FreePercent, 99.8, 100.2); } [Fact] public void InvalidPath_ReturnsUnavailableStateInsteadOfThrowing() { var settings = CreateSettings(enableStorageGuard: true); settings.OutputRoot = "invalid\0path"; var result = _service.CheckCanStartOrResume(settings); Assert.True(result.IsEnabled); Assert.False(result.IsAvailable); Assert.False(result.HasEnoughSpace); Assert.False(result.CanStartNewRecording); Assert.Equal(StorageTier.Red, result.Tier); Assert.Equal(0, result.TotalBytes); } [Fact] public void RedPercentageTier_DoesNotBlockFinalizeWhenAbsoluteTemporarySpaceIsAvailable() { var settings = CreateSettings(enableStorageGuard: true); settings.PauseRecordingWhenFreeSpaceBelowMegabytes = 0; settings.ResumeRecordingWhenFreeSpaceAboveMegabytes = 0; settings.StorageGreenThresholdPercent = 90; settings.StorageRedThresholdPercent = 85; var result = _service.CheckCanFinalize(settings, estimatedTemporaryBytes: 1); Assert.Equal(StorageTier.Red, result.Tier); Assert.True(result.HasEnoughSpace); Assert.True(result.ShouldPauseActive); } [Fact] public void Finalize_ReservesSourceEstimateInAdditionToPauseThreshold() { var settings = CreateSettings(enableStorageGuard: true); settings.PauseRecordingWhenFreeSpaceBelowMegabytes = int.MaxValue; var result = _service.CheckCanFinalize(settings, estimatedTemporaryBytes: 1024); Assert.False(result.HasEnoughSpace); Assert.True(result.RequiredBytes > 1024); } [Fact] public void MultiSegmentFinalize_UsesConcatDemuxerWithoutMaterializingCombinedTs() { var arguments = FfmpegService.BuildMp4FinalizeArgumentList( "/records/input.ffconcat", "/records/output.remux.mp4", strategy: default, useConcatDemuxer: true); Assert.Contains("concat", arguments); Assert.Contains("-safe", arguments); Assert.Contains("/records/input.ffconcat", arguments); Assert.DoesNotContain(".concat.ts", arguments); } private static SystemSettingsDto CreateSettings(bool enableStorageGuard) => new() { OutputRoot = Path.GetTempPath(), EnableStorageGuard = enableStorageGuard, PauseRecordingWhenFreeSpaceBelowMegabytes = 1024, ResumeRecordingWhenFreeSpaceAboveMegabytes = 4096, StorageGreenThresholdPercent = 30, StorageRedThresholdPercent = 10 }; }