using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Paca.Core.Platform;
using Xunit;
namespace Paca.Tests.Core;
///
/// Cycle 3: 극한 RED 시나리오 - 인플레이스 덮어쓰기 원본 파괴 방어, FFmpeg 필터 인젝션 이스케이프, 메타데이터 문법 오염 차단
///
public class MediaProcessingExtremeRedTests
{
private sealed class DummyProcessRunner : IProcessRunner
{
public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) =>
Task.FromResult(new ProcessResult(0, "", ""));
}
[Fact]
public async Task Cycle3_Gate1_InPlaceOverwrite_MustThrowArgumentException_ToPreventSourceDestruction()
{
var runner = new DummyProcessRunner();
var trimmer = new LosslessSmartTrimmer(runner);
var normalizer = new AudioNormalizer(runner);
var burner = new VideoWatermarkBurner(runner);
var path = "C:/media/target_video.mp4";
// 1. 트리머가 동일 경로에 덮어쓰기를 시도하면 원본 파괴(0바이트 절단) 방지를 위해 ArgumentException 발생
var exTrimmer = await Assert.ThrowsAsync(() =>
trimmer.TrimLosslessAsync("ffmpeg.exe", path, path, 0, 10, default));
Assert.Contains("동일", exTrimmer.Message);
// 2. 오디오 정규화기가 동일 경로에 덮어쓰기 시 ArgumentException 발생
var exNormalizer = await Assert.ThrowsAsync(() =>
normalizer.NormalizeAudioAsync("ffmpeg.exe", path, path, LoudnessSpec.WebStandard, default));
Assert.Contains("동일", exNormalizer.Message);
// 3. 워터마크 버너가 동일 경로에 덮어쓰기 시 ArgumentException 발생
var exBurner = await Assert.ThrowsAsync(() =>
burner.ApplyWatermarkAsync("ffmpeg.exe", path, path, new WatermarkSpec("wm.png", WatermarkPosition.TopLeft), default));
Assert.Contains("동일", exBurner.Message);
}
[Fact]
public void Cycle3_Gate2_DrawTextFilter_EscapesSingleQuotesColonsAndBackslashes()
{
// FFmpeg filtergraph syntax: drawtext filter requires escaping for ', :, \, %
var dangerousText = "It's a: sample\\path %date%";
var filter = VideoWatermarkBurner.BuildDrawTextFilter(dangerousText);
// 텍스트 인젝션 방어: 따옴표, 콜론, 백슬래시, 퍼센트가 안전하게 이스케이프되었는지 검증
Assert.DoesNotContain("text='It's", filter); // 원시 따옴표 탈출 불가
Assert.Contains(@"\'", filter); // 따옴표 이스케이프
Assert.Contains(@"\:", filter); // 콜론 이스케이프
Assert.Contains(@"\\", filter); // 백슬래시 이스케이프
Assert.Contains("%%", filter); // 퍼센트 매크로 이스케이프
}
[Fact]
public void Cycle3_Gate3_ChapterSplitter_GenerateFfmetadata_EscapesSpecialCharacters()
{
// FFMETADATA1 포맷 규격: =, ;, #, \, 개행은 백슬래시로 이스케이프되어야 함
var chapters = new List
{
new(1, @"Part#1; Season=2 \ Special" + "\n" + "Subtitle", 0.0, 10.0)
};
var metadata = ChapterSplitter.GenerateFfmetadata(chapters, @"Main=Title; #1 \ Top");
// FFMETADATA1 헤더 검증
Assert.StartsWith(";FFMETADATA1", metadata);
// 특수문자 이스케이프 검증
Assert.Contains(@"Main\=Title\; \#1 \\ Top", metadata);
Assert.Contains(@"Part\#1\; Season\=2 \\ Special", metadata);
}
[Theory]
[InlineData(-10.5, "00:00:00.000")]
[InlineData(-0.001, "00:00:00.000")]
[InlineData(double.NaN, "00:00:00.000")]
[InlineData(double.PositiveInfinity, "99:59:59.999")]
[InlineData(3661.123, "01:01:01.123")]
public void Cycle3_Gate4_VoiceActivitySubtitleAdapter_FormatTimecode_ClampsNegativeAndHandlesDegenerate(double inputSec, string expected)
{
var result = VoiceActivitySubtitleAdapter.FormatTimecode(inputSec);
Assert.Equal(expected, result);
}
}