releases/tests/Paca.Tests/Core/FileNameUtilAndCleanupExtremeRedTests.cs
Yun Chan 8677853822
Some checks failed
release / test (push) Failing after 14m5s
release / windows (push) Has been cancelled
release / android (push) Has been cancelled
release / linux (push) Has been cancelled
release / macos (push) Has been cancelled
test(core/browser/platform): 26 extreme RED/GREEN TDD cycles and full regression hardening (3,235 PASS)
2026-09-18 23:32:32 +09:00

93 lines
3.8 KiB
C#

using System;
using System.IO;
using Paca.Core.Platform;
using Paca.Core.Util;
using Xunit;
namespace Paca.Tests.Core;
/// <summary>
/// Cycle 1: 극한 RED 시나리오 - 파일명 정제 및 윈도우 장치 예약어, 트로이목마 Bidi 스푸핑, 경로 누수 방어 검증
/// </summary>
public class FileNameUtilAndCleanupExtremeRedTests
{
[Theory]
[InlineData("CON_long_stream_title", 3, "_CON")]
[InlineData("PRN_document_capture", 3, "_PRN")]
[InlineData("AUX_recording_session", 3, "_AUX")]
[InlineData("NUL_blackhole_stream", 3, "_NUL")]
[InlineData("COM1_serial_diagnostic", 4, "_COM1")]
[InlineData("LPT1_parallel_port", 4, "_LPT1")]
[InlineData("con.something.video", 3, "_con")]
public void Cycle1_Gate1_TruncatedToReservedName_MustPrependUnderscore(string input, int maxLen, string expectedPrefix)
{
// Act
var sanitized = FileNameUtil.Sanitize(input, maxLen);
// Assert: 윈도우 장치 예약어(CON, PRN, AUX, NUL, COM1-9, LPT1-9)와 일치해서는 안 됨
Assert.StartsWith(expectedPrefix, sanitized, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotMatch(@"^(?i)(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$", sanitized);
}
[Fact]
public void Cycle1_Gate2_BidiTrojanSourceAndZeroWidth_MustBeSanitized()
{
// CVE 클래스: Trojan Source / Unicode Bidi Spoofing
// \u202E (Right-to-Left Override): "video\u202E4pm.exe" -> 화면에는 "videoexe.mp4" 로 착시 유도
var trojanBidi = "my_video\u202E4pm.exe";
var sanitizedBidi = FileNameUtil.Sanitize(trojanBidi);
Assert.DoesNotContain('\u202E', sanitizedBidi);
Assert.DoesNotContain('\u202A', sanitizedBidi);
Assert.DoesNotContain('\u202B', sanitizedBidi);
Assert.DoesNotContain('\u202C', sanitizedBidi);
Assert.DoesNotContain('\u202D', sanitizedBidi);
// 제로 너비 공백(\u200B), BOM(\uFEFF) 등 인비저블 캐릭터 제거
var zeroWidth = "video\u200B\u200C\u200D\uFEFFtitle";
var sanitizedZero = FileNameUtil.Sanitize(zeroWidth);
Assert.DoesNotContain('\u200B', sanitizedZero);
Assert.DoesNotContain('\u200C', sanitizedZero);
Assert.DoesNotContain('\u200D', sanitizedZero);
Assert.DoesNotContain('\uFEFF', sanitizedZero);
}
[Theory]
[InlineData("............", 5, "video")]
[InlineData(" ", 8, "video")]
[InlineData("valid_name", 0, "video")]
[InlineData("valid_name", -5, "video")]
[InlineData("...", 1, "video")]
public void Cycle1_Gate3_DegenerateMaxLenAndDotSpaceCollapse_MustFallbackToVideo(string input, int maxLen, string expected)
{
// Act
var result = FileNameUtil.Sanitize(input, maxLen);
// Assert: 빈 문자열이나 ArgumentOutOfRangeException 없이 안전한 기본값 반환
Assert.Equal(expected, result);
}
[Fact]
public void Cycle1_Gate4_DownloadMetaPathResolver_TempAndParts_MustBeInPacaHiddenDirectory()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "paca_test_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
try
{
var outputBase = Path.Combine(tempRoot, "sample_video");
var partsDir = DownloadMetaPathResolver.GetPartsDirectory(outputBase);
var tempPartFile = DownloadMetaPathResolver.GetTempPartFilePath(outputBase);
// .paca 격리 서브디렉터리 내부에 생성되어야 함
Assert.Contains(".paca", partsDir);
Assert.Contains(".paca", tempPartFile);
Assert.EndsWith("sample_video.parts", partsDir);
Assert.EndsWith("sample_video.part", tempPartFile);
}
finally
{
try { if (Directory.Exists(tempRoot)) Directory.Delete(tempRoot, true); } catch { }
}
}
}