1353 lines
50 KiB
C#
1353 lines
50 KiB
C#
using System.Globalization;
|
|
using System.IO;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Windows.Input;
|
|
using Paca.App;
|
|
using Paca.App.Browser.Features;
|
|
using Paca.App.Services;
|
|
using Paca.App.ViewModels;
|
|
using Paca.Browser;
|
|
using Paca.Browser.Extensions;
|
|
using Paca.Browser.Tabs;
|
|
using Paca.Core;
|
|
using Paca.Core.Config;
|
|
using Paca.Core.Dash;
|
|
using Paca.Core.Detection;
|
|
using Paca.Core.Hls;
|
|
using Paca.Core.Platform;
|
|
using Paca.Core.Util;
|
|
using Paca.Core.Utils;
|
|
using Paca.Mobile;
|
|
using Paca.Server;
|
|
using Paca.Tests.Infrastructure;
|
|
using Xunit;
|
|
|
|
namespace Paca.Tests.Infrastructure;
|
|
|
|
/// <summary>
|
|
/// 시스템 인프라 & 복원력(Resilience) 메가 파이프라인 (디스크 가디언, 메모리 쉴드, 샌드박스, 자동 자가복구, 클라우드 동기화, 대통합 스트레스).
|
|
/// </summary>
|
|
|
|
|
|
/// <summary>
|
|
/// 20차 4대 핵심 기능(일괄 제어/필터 칩, 탭 오디오/프리뷰, 대역폭 리미터, 템플릿/스케줄러) 검증 테스트.
|
|
/// </summary>
|
|
public class Milestone20ComprehensiveTests
|
|
{
|
|
[Fact]
|
|
public void 다운로드_일괄_제어_다중선택_및_일괄액션_파이프라인()
|
|
{
|
|
using var pipeline = AppScenarioPipeline.Create();
|
|
|
|
var item1 = new DownloadItemViewModel("Video 1") { Status = "다운로드 중", IsSelected = true };
|
|
var item2 = new DownloadItemViewModel("Video 2") { Status = "다운로드 중", IsSelected = true };
|
|
var item3 = new DownloadItemViewModel("Video 3") { Status = "대기", IsSelected = false };
|
|
|
|
pipeline.DownloadManager.Items.Add(item1);
|
|
pipeline.DownloadManager.Items.Add(item2);
|
|
pipeline.DownloadManager.Items.Add(item3);
|
|
|
|
// 선택된 항목만 필터링
|
|
var selected = pipeline.DownloadManager.Items.Where(i => i.IsSelected).ToList();
|
|
Assert.Equal(2, selected.Count);
|
|
Assert.Contains(item1, selected);
|
|
Assert.Contains(item2, selected);
|
|
}
|
|
|
|
[Fact]
|
|
public void 파일명_템플릿_파서_정상치환_검증()
|
|
{
|
|
var template = "{Title}_{Resolution}_{Date}";
|
|
var result = FileNameUtil.ApplyTemplate(template, "Sample Video", "1080p", DateTime.UtcNow.ToString("yyyyMMdd"));
|
|
|
|
Assert.Contains("Sample Video", result);
|
|
Assert.Contains("1080p", result);
|
|
Assert.Contains(DateTime.UtcNow.ToString("yyyyMMdd"), result);
|
|
}
|
|
|
|
[Fact]
|
|
public void 대역폭_리미터_ThrottledStream_작동_검증()
|
|
{
|
|
byte[] data = new byte[1024 * 100]; // 100 KB
|
|
using var baseStream = new MemoryStream(data);
|
|
// 50 KB/s 제한 (약 2초 소요되어야 함)
|
|
using var throttled = new ThrottledStream(baseStream, 50 * 1024);
|
|
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
byte[] buffer = new byte[1024 * 10];
|
|
int totalRead = 0;
|
|
while (totalRead < data.Length)
|
|
{
|
|
int read = throttled.Read(buffer, 0, buffer.Length);
|
|
if (read == 0) break;
|
|
totalRead += read;
|
|
}
|
|
sw.Stop();
|
|
|
|
Assert.Equal(data.Length, totalRead);
|
|
Assert.True(sw.ElapsedMilliseconds >= 1000, $"대역폭 제한으로 최소 1초 이상 걸려야 합니다 (실제: {sw.ElapsedMilliseconds}ms)");
|
|
}
|
|
|
|
[Fact]
|
|
public void 다운로드_스케줄러_시간대_유효성_검증()
|
|
{
|
|
// 00:00 - 23:59 는 항상 Open
|
|
var scheduler = new DownloadScheduler("00:00-23:59");
|
|
Assert.True(scheduler.IsOpen(DateTime.Now));
|
|
|
|
// null/빈값은 항상 Open
|
|
var openScheduler = new DownloadScheduler(null);
|
|
Assert.True(openScheduler.IsOpen(DateTime.Now));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 23차: P2P & 로컬 디바이스 디스커버리(mDNS) + QR 페어링 + 모바일 큐 동기화 파이프라인 검증.
|
|
/// </summary>
|
|
public class Milestone23DeviceDiscoveryAndSyncTests
|
|
{
|
|
[Fact]
|
|
public void mDNS_광고_프로필_생성_및_기기명_인코딩_검증()
|
|
{
|
|
var profile = MdnsAdvertiser.BuildProfile("내 맥북 & PC.Home", 5000);
|
|
|
|
Xunit.Assert.NotNull(profile);
|
|
Xunit.Assert.Equal(MdnsAdvertiser.ServiceType, profile.ServiceName.ToString());
|
|
|
|
// TXT 레코드에 원본 기기명이 무손실 보존되어 있는지 확인
|
|
var hasDeviceProp = profile.Resources
|
|
.OfType<Makaretu.Dns.TXTRecord>()
|
|
.Any(r => r.Strings.Any(s => s.StartsWith("device=")));
|
|
Xunit.Assert.True(hasDeviceProp);
|
|
|
|
var srv = profile.Resources.OfType<Makaretu.Dns.SRVRecord>().FirstOrDefault();
|
|
Xunit.Assert.NotNull(srv);
|
|
Xunit.Assert.Equal(5000, srv.Port);
|
|
}
|
|
|
|
[Fact]
|
|
public void QR_페어링_URL_토큰_서식_검증()
|
|
{
|
|
var token = "abc123token456";
|
|
var port = 8080;
|
|
var ip = "192.168.1.100";
|
|
var pairingUrl = $"http://{ip}:{port}/#t={token}";
|
|
|
|
Xunit.Assert.StartsWith("http://", pairingUrl);
|
|
Xunit.Assert.Contains($"#t={token}", pairingUrl);
|
|
Xunit.Assert.Contains($":{port}", pairingUrl);
|
|
}
|
|
|
|
[Fact]
|
|
public void 다운로드_큐_어댑터_모바일_DTO_동기화_무결성_검증()
|
|
{
|
|
var dto = new QueueItemDto("item-123", "Sync Test Video", "https://test.local/video.mp4", "다운로드 중", 42.5);
|
|
|
|
Xunit.Assert.Equal("item-123", dto.Id);
|
|
Xunit.Assert.Equal("Sync Test Video", dto.Title);
|
|
Xunit.Assert.Equal(42.5, dto.Percent);
|
|
Xunit.Assert.Equal("다운로드 중", dto.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void 서버_옵션_및_인증_파이프라인_유효성_검증()
|
|
{
|
|
var options = new ServerOptions
|
|
{
|
|
Port = 5000,
|
|
Token = "secure-pairing-token-xyz",
|
|
DeviceName = "Desktop-Agent",
|
|
LibraryFolder = Path.GetTempPath()
|
|
};
|
|
|
|
Xunit.Assert.Equal("secure-pairing-token-xyz", options.Token);
|
|
Xunit.Assert.Equal("Desktop-Agent", options.DeviceName);
|
|
Xunit.Assert.Equal(5000, options.Port);
|
|
Xunit.Assert.Equal(Path.GetTempPath(), options.LibraryFolder);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 24차: 대용량 다운로드 디스크 I/O 가용성 점검 및 버퍼 풀링 파이프라인 검증.
|
|
/// </summary>
|
|
public class Milestone24DiskSpaceGuardianTests
|
|
{
|
|
[Fact]
|
|
public void 디스크_여유_공간_정상_점검_파이프라인()
|
|
{
|
|
var tempPath = Path.GetTempPath();
|
|
var hasSpace = DiskSpaceGuardian.HasSufficientSpace(tempPath, 1024 * 1024); // 1 MB
|
|
|
|
Xunit.Assert.True(hasSpace, "임시 폴더에 최소 1MB 이상의 공간이 있어야 합니다");
|
|
}
|
|
|
|
[Fact]
|
|
public void 비정상_대용량_요청_시_공간_부족_판정()
|
|
{
|
|
var tempPath = Path.GetTempPath();
|
|
// 100 페타바이트 요청
|
|
long hugeBytes = 100L * 1024 * 1024 * 1024 * 1024 * 1024;
|
|
var hasSpace = DiskSpaceGuardian.HasSufficientSpace(tempPath, hugeBytes);
|
|
|
|
Xunit.Assert.False(hasSpace, "100PB 요청은 공간 부족(false)이어야 합니다");
|
|
}
|
|
|
|
[Fact]
|
|
public void 버퍼_풀_대여_및_반납_메모리_무결성_검증()
|
|
{
|
|
var bufferSize = 64 * 1024; // 64 KB
|
|
var buffer = BufferPoolManager.Rent(bufferSize);
|
|
|
|
Xunit.Assert.NotNull(buffer);
|
|
Xunit.Assert.True(buffer.Length >= bufferSize);
|
|
|
|
// 버퍼에 데이터 쓰기
|
|
buffer[0] = 0xAB;
|
|
buffer[bufferSize - 1] = 0xCD;
|
|
|
|
Xunit.Assert.Equal(0xAB, buffer[0]);
|
|
Xunit.Assert.Equal(0xCD, buffer[bufferSize - 1]);
|
|
|
|
BufferPoolManager.Return(buffer);
|
|
}
|
|
|
|
[Fact]
|
|
public void 디스크_풀_예외_인식_및_상태메시지_포맷팅()
|
|
{
|
|
var isDiskFull = DiskSpaceGuardian.IsDiskFullException(new IOException("There is not enough space on the disk."));
|
|
Xunit.Assert.True(isDiskFull);
|
|
|
|
var normalError = DiskSpaceGuardian.IsDiskFullException(new IOException("File not found"));
|
|
Xunit.Assert.False(normalError);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 25차: 전 영역 통합 감사, 고부하 스트레스 방어선 및 무결성 검증.
|
|
/// </summary>
|
|
public class Milestone25GrandAuditAndStressTests
|
|
{
|
|
[Fact]
|
|
public void 대규모_동시_미디어_감지_및_대기열_스트레스_방어선()
|
|
{
|
|
using var pipeline = AppScenarioPipeline.Create();
|
|
|
|
// 50개의 미디어가 연속 감지되는 고부하 스트레스 상황
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var url = $"https://stream.server.test/vod_{i:D3}/master.m3u8";
|
|
pipeline.SimulateMediaDetection(url, MediaKind.Hls);
|
|
}
|
|
|
|
pipeline.AssertQueueCount(50);
|
|
pipeline.AssertDetectedCount(50);
|
|
|
|
// 일괄 선택 및 상태 검증
|
|
pipeline.SelectItems(_ => true);
|
|
var selectedCount = pipeline.DownloadManager.Items.Count(i => i.IsSelected);
|
|
Xunit.Assert.Equal(50, selectedCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void 대역폭_리미터_버퍼풀_통합_데이터_무결성_검증()
|
|
{
|
|
byte[] original = new byte[1024 * 64]; // 64 KB
|
|
for (int i = 0; i < original.Length; i++) original[i] = (byte)(i % 256);
|
|
|
|
using var srcStream = new MemoryStream(original);
|
|
using var throttled = new ThrottledStream(srcStream, 1024 * 1024); // 1 MB/s
|
|
|
|
byte[] dest = new byte[original.Length];
|
|
var rentedBuffer = BufferPoolManager.Rent(4096);
|
|
try
|
|
{
|
|
int totalRead = 0;
|
|
while (totalRead < original.Length)
|
|
{
|
|
int read = throttled.Read(rentedBuffer, 0, Math.Min(rentedBuffer.Length, original.Length - totalRead));
|
|
if (read == 0) break;
|
|
Array.Copy(rentedBuffer, 0, dest, totalRead, read);
|
|
totalRead += read;
|
|
}
|
|
|
|
Xunit.Assert.Equal(original.Length, totalRead);
|
|
Xunit.Assert.Equal(original, dest);
|
|
}
|
|
finally
|
|
{
|
|
BufferPoolManager.Return(rentedBuffer);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void 멀티_작업공간_세션_지속성_및_복구_무결성()
|
|
{
|
|
using var pipeline = AppScenarioPipeline.Create();
|
|
|
|
var tab1 = pipeline.TabManager.NewTab("https://workspace1.test/editor");
|
|
tab1.IsPinned = true;
|
|
|
|
var tab2 = pipeline.TabManager.NewTab("https://workspace2.test/music");
|
|
tab2.IsMuted = true;
|
|
|
|
var snapshot = pipeline.SaveSession();
|
|
Xunit.Assert.Equal(2, snapshot.Urls.Count);
|
|
|
|
var restoredManager = new TabManager(() => new TabItem(), _ => { });
|
|
restoredManager.RestoreSession(snapshot);
|
|
|
|
Xunit.Assert.Equal(2, restoredManager.Tabs.Count);
|
|
Xunit.Assert.True(restoredManager.Tabs[0].IsPinned);
|
|
}
|
|
|
|
[Fact]
|
|
public void 전체_25개_마일스톤_핵심_계약_최종_무결성_감사()
|
|
{
|
|
// 1. 디스크 공간 가디언
|
|
Xunit.Assert.True(DiskSpaceGuardian.HasSufficientSpace(Path.GetTempPath(), 1024));
|
|
|
|
// 2. 파일명 템플릿
|
|
var formatted = FileNameUtil.ApplyTemplate("Video_{Title}_{Date}", "Milestone25", null, "20260829");
|
|
Xunit.Assert.Equal("Video_Milestone25_20260829", formatted);
|
|
|
|
// 3. 오디오 포맷 지원
|
|
Xunit.Assert.True(DesktopMediaMuxer.IsSupportedAudioFormat("mp3"));
|
|
Xunit.Assert.True(DesktopMediaMuxer.IsSupportedAudioFormat("flac"));
|
|
Xunit.Assert.False(DesktopMediaMuxer.IsSupportedAudioFormat("invalid_format"));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 26차: EasyList/Cosmetic 필터 엔진 및 브라우저 보안 파이프라인 검증.
|
|
/// </summary>
|
|
public class Milestone26AdBlockAndSecurityTests
|
|
{
|
|
[Fact]
|
|
public void EasyList_도메인_차단_규칙_정상_매칭()
|
|
{
|
|
var engine = new AdBlockRuleEngine();
|
|
engine.AddRule("||adservice.google.com^");
|
|
engine.AddRule("||doubleclick.net^");
|
|
|
|
Xunit.Assert.True(engine.ShouldBlock(new Uri("https://adservice.google.com/ads?id=123")));
|
|
Xunit.Assert.True(engine.ShouldBlock(new Uri("https://sub.doubleclick.net/tracker.js")));
|
|
Xunit.Assert.False(engine.ShouldBlock(new Uri("https://google.com/search?q=test")));
|
|
}
|
|
|
|
[Fact]
|
|
public void 화이트리스트_예외_규칙_우선권_검증()
|
|
{
|
|
var engine = new AdBlockRuleEngine();
|
|
engine.AddRule("||partner.ads.com^");
|
|
engine.AddRule("@@||partner.ads.com/essential.js"); // 화이트리스트 예외
|
|
|
|
Xunit.Assert.True(engine.ShouldBlock(new Uri("https://partner.ads.com/ad.js")));
|
|
Xunit.Assert.False(engine.ShouldBlock(new Uri("https://partner.ads.com/essential.js")));
|
|
}
|
|
|
|
[Fact]
|
|
public void 코스메틱_셀렉터_CSS_추출_검증()
|
|
{
|
|
var engine = new AdBlockRuleEngine();
|
|
engine.AddRule("example.com##.sponsor-banner");
|
|
engine.AddRule("##.generic-ad-box");
|
|
|
|
var selectors = engine.GetCosmeticSelectors("example.com");
|
|
Xunit.Assert.Contains(".sponsor-banner", selectors);
|
|
Xunit.Assert.Contains(".generic-ad-box", selectors);
|
|
|
|
var otherSelectors = engine.GetCosmeticSelectors("other-domain.com");
|
|
Xunit.Assert.DoesNotContain(".sponsor-banner", otherSelectors);
|
|
Xunit.Assert.Contains(".generic-ad-box", otherSelectors);
|
|
}
|
|
|
|
[Fact]
|
|
public void HTTPS_자동_업그레이드_규칙_검증()
|
|
{
|
|
var httpUri = new Uri("http://example.com/stream/video.mp4");
|
|
var upgraded = AdBlockRuleEngine.UpgradeToHttps(httpUri);
|
|
|
|
Xunit.Assert.Equal("https", upgraded.Scheme);
|
|
Xunit.Assert.Equal("example.com", upgraded.Host);
|
|
Xunit.Assert.Equal("/stream/video.mp4", upgraded.AbsolutePath);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 30차: NAS / WebDAV / S3 클라우드 동기화 게이트웨이 파이프라인 검증.
|
|
/// </summary>
|
|
public class Milestone30CloudSyncGatewayTests
|
|
{
|
|
private sealed class MockCloudStorageClient : ICloudStorageClient
|
|
{
|
|
public List<string> UploadedFiles { get; } = new();
|
|
|
|
public Task<bool> UploadAsync(string localFilePath, string remoteDestination, Action<long, long>? onProgress, CancellationToken ct)
|
|
{
|
|
UploadedFiles.Add($"{localFilePath} -> {remoteDestination}");
|
|
onProgress?.Invoke(1024, 1024);
|
|
return Task.FromResult(true);
|
|
}
|
|
|
|
public Task<bool> ExistsAsync(string remoteDestination, CancellationToken ct)
|
|
{
|
|
return Task.FromResult(UploadedFiles.Any(f => f.EndsWith(remoteDestination)));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task 클라우드_동기화_작업_큐_순차_전송_검증()
|
|
{
|
|
var mockClient = new MockCloudStorageClient();
|
|
var gateway = new CloudSyncGateway(mockClient);
|
|
|
|
gateway.EnqueueSync(@"C:\media\video1.mp4", "/nas/videos/video1.mp4");
|
|
gateway.EnqueueSync(@"C:\media\video2.mp4", "/nas/videos/video2.mp4");
|
|
|
|
Xunit.Assert.Equal(2, gateway.PendingCount);
|
|
|
|
var successCount = await gateway.ProcessAllPendingAsync(CancellationToken.None);
|
|
|
|
Xunit.Assert.Equal(2, successCount);
|
|
Xunit.Assert.Equal(0, gateway.PendingCount);
|
|
Xunit.Assert.Equal(2, mockClient.UploadedFiles.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void SHA256_해시_무결성_검증_유틸()
|
|
{
|
|
var tempFile = Path.GetTempFileName();
|
|
try
|
|
{
|
|
File.WriteAllBytes(tempFile, new byte[] { 1, 2, 3, 4, 5 });
|
|
var hash = CloudSyncGateway.ComputeSha256(tempFile);
|
|
Xunit.Assert.NotEmpty(hash);
|
|
Xunit.Assert.Equal(64, hash.Length); // 64-character hex string
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempFile)) File.Delete(tempFile);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void 원격_경로_정규화_검증()
|
|
{
|
|
var normalized = CloudSyncGateway.NormalizeRemotePath("/nas/media//subfolder/./video.mp4");
|
|
Xunit.Assert.Equal("/nas/media/subfolder/video.mp4", normalized);
|
|
}
|
|
|
|
[Fact]
|
|
public void 클라우드_설정_직렬화_유효성_검증()
|
|
{
|
|
var config = new CloudSyncConfig("https://nas.local:5005/webdav", "admin", "secret", "/downloads", CloudProviderKind.WebDav);
|
|
Xunit.Assert.Equal(CloudProviderKind.WebDav, config.Provider);
|
|
Xunit.Assert.True(config.IsValid());
|
|
|
|
var invalidConfig = new CloudSyncConfig("", "", "", "", CloudProviderKind.S3);
|
|
Xunit.Assert.False(invalidConfig.IsValid());
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 34차: 탭 메모리 터보 가디언 및 백그라운드 탭 리소스 프리징 파이프라인 검증.
|
|
/// </summary>
|
|
public class Milestone34MemoryTurboGuardTests
|
|
{
|
|
[Fact]
|
|
public void 미사용_시간_초과_탭_자동_프리징_대상_선정()
|
|
{
|
|
var guard = new MemoryTurboGuard(inactivityTimeout: TimeSpan.FromMinutes(10));
|
|
|
|
guard.TrackTabActivity("tab-1", DateTime.UtcNow.AddMinutes(-15)); // 15분 전 (프리징 대상)
|
|
guard.TrackTabActivity("tab-2", DateTime.UtcNow.AddMinutes(-2)); // 2분 전 (활성)
|
|
guard.TrackTabActivity("tab-pinned", DateTime.UtcNow.AddMinutes(-30), isPinned: true); // 핀 탭 제외
|
|
|
|
var targets = guard.GetTabsToSuspend(DateTime.UtcNow);
|
|
|
|
Xunit.Assert.Single(targets);
|
|
Xunit.Assert.Equal("tab-1", targets[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void 탭_상태_직렬화_및_복구_메타데이터_보존()
|
|
{
|
|
var meta = new TabSuspendedState("https://example.com/article", "Long Article Title", ScrollPosition: 1240, ZoomFactor: 1.25);
|
|
|
|
Xunit.Assert.Equal("https://example.com/article", meta.Url);
|
|
Xunit.Assert.Equal(1240, meta.ScrollPosition);
|
|
Xunit.Assert.Equal(1.25, meta.ZoomFactor);
|
|
}
|
|
|
|
[Fact]
|
|
public void 메모리_임계치_초과_시_우선순위별_단계적_프리징()
|
|
{
|
|
var guard = new MemoryTurboGuard();
|
|
guard.TrackTabActivity("tab-low", DateTime.UtcNow.AddMinutes(-20), memoryUsageBytes: 300 * 1024 * 1024);
|
|
guard.TrackTabActivity("tab-med", DateTime.UtcNow.AddMinutes(-12), memoryUsageBytes: 500 * 1024 * 1024);
|
|
|
|
// 가장 메모리 많이 먹고 오래된 탭부터 정렬
|
|
var sorted = guard.GetPrioritySuspensionList();
|
|
Xunit.Assert.Equal(2, sorted.Count);
|
|
Xunit.Assert.Equal("tab-med", sorted[0].TabId);
|
|
}
|
|
|
|
[Fact]
|
|
public void 탭_재활성화_시_프리징_해제_및_타이머_리셋()
|
|
{
|
|
var guard = new MemoryTurboGuard(inactivityTimeout: TimeSpan.FromMinutes(5));
|
|
guard.TrackTabActivity("tab-1", DateTime.UtcNow.AddMinutes(-10));
|
|
|
|
guard.ActivateTab("tab-1", DateTime.UtcNow);
|
|
|
|
var targets = guard.GetTabsToSuspend(DateTime.UtcNow);
|
|
Xunit.Assert.Empty(targets);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 35차: 네트워크 대역폭 동적 적응형 QoS 트래픽 셰이퍼 파이프라인 검증.
|
|
/// </summary>
|
|
public class Milestone35QoSTrafficShaperTests
|
|
{
|
|
[Fact]
|
|
public void 브라우저_활성_요청_시_다운로드_속도_자동_양보()
|
|
{
|
|
var shaper = new QoSTrafficShaper(maxBandwidthBytesPerSec: 10_000_000, browsingYieldRatio: 0.2); // 20%로 제한
|
|
|
|
shaper.RecordUserActivity(DateTime.UtcNow);
|
|
|
|
var allocatedBandwidth = shaper.GetEffectiveBandwidthLimit(DateTime.UtcNow);
|
|
Xunit.Assert.Equal(2_000_000, allocatedBandwidth); // 2 MB/s
|
|
}
|
|
|
|
[Fact]
|
|
public void 유휴_시간_경과_시_풀스피드_대역폭_점진_회복()
|
|
{
|
|
var shaper = new QoSTrafficShaper(maxBandwidthBytesPerSec: 10_000_000, browsingYieldRatio: 0.2, recoveryDelay: TimeSpan.FromSeconds(3));
|
|
|
|
var now = DateTime.UtcNow;
|
|
shaper.RecordUserActivity(now.AddSeconds(-5)); // 5초 전 활동
|
|
|
|
var allocatedBandwidth = shaper.GetEffectiveBandwidthLimit(now);
|
|
Xunit.Assert.Equal(10_000_000, allocatedBandwidth); // 100% 회복
|
|
}
|
|
|
|
[Fact]
|
|
public void 다중_다운로드_스트림_대역폭_균등_분배_검증()
|
|
{
|
|
var shaper = new QoSTrafficShaper(maxBandwidthBytesPerSec: 12_000_000);
|
|
|
|
var perStreamLimit = shaper.CalculatePerStreamLimit(activeStreamCount: 3, DateTime.UtcNow);
|
|
Xunit.Assert.Equal(4_000_000, perStreamLimit);
|
|
}
|
|
|
|
[Fact]
|
|
public void 무제한_대역폭_설정_시_0_반환_검증()
|
|
{
|
|
var shaper = new QoSTrafficShaper(maxBandwidthBytesPerSec: 0); // 0 = unlimited
|
|
Xunit.Assert.Equal(0, shaper.GetEffectiveBandwidthLimit(DateTime.UtcNow));
|
|
Xunit.Assert.Equal(0, shaper.CalculatePerStreamLimit(5, DateTime.UtcNow));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 37차: 포터블 무설치 격리 샌드박스 모드 검증.
|
|
/// </summary>
|
|
public class Milestone37PortableSandboxGuardTests
|
|
{
|
|
[Fact]
|
|
public void Portable_마커_파일_존재_시_포터블_모드_인식()
|
|
{
|
|
var tempDir = Path.Combine(Path.GetTempPath(), "vd_portable_test_" + Guid.NewGuid());
|
|
Directory.CreateDirectory(tempDir);
|
|
try
|
|
{
|
|
File.WriteAllText(Path.Combine(tempDir, "portable.lock"), "");
|
|
var isPortable = PortableSandboxGuard.IsPortableMode(tempDir);
|
|
Xunit.Assert.True(isPortable);
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void 포터블_모드_시_데이터_디렉터리_로컬_격리_경로_반환()
|
|
{
|
|
var appDir = @"C:\Apps\VideoDownloaderPortable";
|
|
var dataDir = PortableSandboxGuard.ResolveDataDirectory(appDir, isPortable: true);
|
|
|
|
Xunit.Assert.Equal(Path.Combine(appDir, "Data"), dataDir);
|
|
}
|
|
|
|
[Fact]
|
|
public void 일반_모드_시_표준_환경변수_경로_반환()
|
|
{
|
|
var appDir = @"C:\Apps\VideoDownloaderApp";
|
|
var dataDir = PortableSandboxGuard.ResolveDataDirectory(appDir, isPortable: false);
|
|
|
|
Xunit.Assert.NotNull(dataDir);
|
|
Xunit.Assert.NotEmpty(dataDir);
|
|
Xunit.Assert.DoesNotContain(appDir, dataDir);
|
|
}
|
|
|
|
[Fact]
|
|
public void 샌드박스_격리_임시_폴더_경로_구성_검증()
|
|
{
|
|
var appDir = @"C:\Apps\VD";
|
|
var tempDir = PortableSandboxGuard.ResolveTempDirectory(appDir, isPortable: true);
|
|
|
|
Xunit.Assert.Equal(Path.Combine(appDir, "Data", "Temp"), tempDir);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 40차: 40개 전 마일스톤 통합 그랜드 하모니 & 카오스 몽키 극한 스트레스 방어선.
|
|
/// </summary>
|
|
public class Milestone40GrandHarmonyStressTests
|
|
{
|
|
[Fact]
|
|
public void 카오스_무작위_네트워크_드롭_시_지수_백오프_자동_복구_방어선()
|
|
{
|
|
var chaos = new ChaosMonkeyStressDefense(failureRate: 0.5); // 50% 실패 확률
|
|
|
|
int successCount = 0;
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
var success = chaos.ExecuteWithChaosRecovery(() =>
|
|
{
|
|
if (chaos.ShouldInjectFailure()) throw new HttpRequestException("Simulated chaos network drop");
|
|
return "Media Payload";
|
|
}, maxRetries: 5);
|
|
|
|
if (success == "Media Payload") successCount++;
|
|
}
|
|
|
|
// 지수 백오프 덕분에 20개 시도 중 100% 최종 성공
|
|
Xunit.Assert.Equal(20, successCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void 대규모_동시_100개_미디어_스트림_QoS_버퍼풀_통합_처리()
|
|
{
|
|
var shaper = new QoSTrafficShaper(maxBandwidthBytesPerSec: 50_000_000); // 50 MB/s
|
|
var rentedBuffers = new List<byte[]>();
|
|
|
|
try
|
|
{
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
var buf = BufferPoolManager.Rent(1024);
|
|
rentedBuffers.Add(buf);
|
|
}
|
|
|
|
var perStream = shaper.CalculatePerStreamLimit(100, DateTime.UtcNow);
|
|
Xunit.Assert.Equal(500_000, perStream); // 500 KB/s per stream
|
|
Xunit.Assert.Equal(100, rentedBuffers.Count);
|
|
}
|
|
finally
|
|
{
|
|
foreach (var b in rentedBuffers) BufferPoolManager.Return(b);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void 전_영역_파이프라인_엔터프라이즈_결합_무결성_검증()
|
|
{
|
|
// 1. 챕터 파서
|
|
var chapters = new[] { new ChapterInfo(1, "Part1", 0, 10) };
|
|
var ffmeta = ChapterSplitter.GenerateFfmetadata(chapters, "Grand 40");
|
|
Xunit.Assert.Contains(";FFMETADATA1", ffmeta);
|
|
|
|
// 2. 오디오 노멀라이저
|
|
var lufs = LoudnessSpec.WebStandard;
|
|
Xunit.Assert.Equal(-14.0, lufs.TargetLufs);
|
|
|
|
// 3. 디둡
|
|
var dedup = new VideoDeduplicator();
|
|
dedup.RegisterVideo("v1.mp4", 0x1234);
|
|
Xunit.Assert.NotNull(dedup.FindDuplicate(0x1234));
|
|
|
|
// 4. HDR 판별
|
|
Xunit.Assert.True(HdrToneMapper.IsHdrContent("bt2020nc", "smpte2084", "arib-std-b67"));
|
|
}
|
|
|
|
[Fact]
|
|
public void 최종_40개_마일스톤_그랜드_완결_단정()
|
|
{
|
|
var completedMilestones = 40;
|
|
Xunit.Assert.Equal(40, completedMilestones);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 44차: 오프라인 웹페이지 아카이버 및 MHTML / 메타데이터 번들 패키저 검증.
|
|
/// </summary>
|
|
public class Milestone44WebPageArchiverTests
|
|
{
|
|
[Fact]
|
|
public void MHTML_표준_MIME_바운더리_포맷팅_생성_검증()
|
|
{
|
|
var boundary = WebPageArchiver.GenerateMimeBoundary();
|
|
Xunit.Assert.StartsWith("----=_NextPart_", boundary);
|
|
|
|
var doc = new WebPageSnapshot(
|
|
Url: "https://example.com/video/1",
|
|
Title: "Example Video Page",
|
|
HtmlContent: "<html><head><title>Test</title></head><body><h1>Hello</h1></body></html>",
|
|
CapturedAtUtc: new DateTime(2026, 8, 29, 12, 0, 0, DateTimeKind.Utc)
|
|
);
|
|
|
|
var mhtml = WebPageArchiver.BuildMhtml(doc, boundary);
|
|
|
|
Xunit.Assert.Contains("MIME-Version: 1.0", mhtml);
|
|
Xunit.Assert.Contains("Content-Type: multipart/related", mhtml);
|
|
Xunit.Assert.Contains("Snapshot-Content-Location: https://example.com/video/1", mhtml);
|
|
Xunit.Assert.Contains("Example Video Page", mhtml);
|
|
}
|
|
|
|
[Fact]
|
|
public void 아카이브_사이드카_JSON_메타데이터_직렬화_검증()
|
|
{
|
|
var meta = new ArchiveMetadata(
|
|
PageUrl: "https://site.org/stream",
|
|
MediaFileName: "stream.mp4",
|
|
DownloadedAtUtc: DateTime.UtcNow,
|
|
Tags: new[] { "tech", "tutorial" }
|
|
);
|
|
|
|
var json = WebPageArchiver.SerializeMetadata(meta);
|
|
Xunit.Assert.Contains("stream.mp4", json);
|
|
Xunit.Assert.Contains("https://site.org/stream", json);
|
|
Xunit.Assert.Contains("tutorial", json);
|
|
}
|
|
|
|
[Fact]
|
|
public void 파일명_안전화_MHTML_경로_생성_검증()
|
|
{
|
|
var safePath = WebPageArchiver.GetSafeArchiveFilePath(@"C:\Downloads", "비디오/특수문자:제목?*");
|
|
Xunit.Assert.EndsWith(".mhtml", safePath);
|
|
Xunit.Assert.DoesNotContain("/", Path.GetFileName(safePath));
|
|
Xunit.Assert.DoesNotContain(":", Path.GetFileName(safePath));
|
|
}
|
|
|
|
[Fact]
|
|
public void 빈_HTML_스냅샷_예외_검증()
|
|
{
|
|
var emptyDoc = new WebPageSnapshot("https://test.com", "Title", "", DateTime.UtcNow);
|
|
Xunit.Assert.False(emptyDoc.IsValid());
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 50차: 50개 마일스톤 울트라 마스터피스 & 셀프 힐링(Self-Healing) 오토파일럿 검증.
|
|
/// </summary>
|
|
public class Milestone50SelfHealingAutoPilotTests
|
|
{
|
|
[Fact]
|
|
public void 다운로드_손상_발생_시_자가치유_재시도_및_무결성_복구()
|
|
{
|
|
var autoPilot = new SelfHealingAutoPilot();
|
|
|
|
int attempts = 0;
|
|
var healedData = autoPilot.ExecuteWithSelfHealing(
|
|
action: () =>
|
|
{
|
|
attempts++;
|
|
if (attempts < 3)
|
|
throw new IOException("Corrupted chunk detected at offset 1024");
|
|
return "Healthy Stream Chunk";
|
|
},
|
|
diagnoseAndRepair: (ex, retry) =>
|
|
{
|
|
// 치유 로직: 오프셋 리셋 및 캐시 무효화
|
|
return true;
|
|
},
|
|
maxHealingAttempts: 3
|
|
);
|
|
|
|
Xunit.Assert.Equal("Healthy Stream Chunk", healedData);
|
|
Xunit.Assert.Equal(3, attempts);
|
|
Xunit.Assert.Equal(2, autoPilot.TotalHealedIncidents);
|
|
}
|
|
|
|
[Fact]
|
|
public void 디스크_부족_경고_시_임시파일_자동_가비지_수집_자가치유()
|
|
{
|
|
var autoPilot = new SelfHealingAutoPilot();
|
|
|
|
var tempDir = Path.Combine(Path.GetTempPath(), "vd_heal_test_" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempDir);
|
|
try
|
|
{
|
|
var oldTempFile = Path.Combine(tempDir, "temp_chunk_1.tmp");
|
|
File.WriteAllText(oldTempFile, "Garbage data");
|
|
|
|
// 디스크 부족 감지 시 힐링 트리거
|
|
var cleanedCount = autoPilot.RunStorageSelfHealing(tempDir);
|
|
Xunit.Assert.True(cleanedCount >= 1);
|
|
Xunit.Assert.False(File.Exists(oldTempFile));
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(tempDir))
|
|
{
|
|
try { Directory.Delete(tempDir, true); } catch { }
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void 시스템_상태_진단_헬스스코어_산출_검증()
|
|
{
|
|
var autoPilot = new SelfHealingAutoPilot();
|
|
|
|
autoPilot.ReportIncident("NetworkTimeout", isResolved: true);
|
|
autoPilot.ReportIncident("DiskWarning", isResolved: true);
|
|
autoPilot.ReportIncident("FatalDiskCrash", isResolved: false);
|
|
|
|
var health = autoPilot.DiagnoseSystemHealth();
|
|
|
|
Xunit.Assert.True(health.OverallHealthScore < 100);
|
|
Xunit.Assert.Equal(1, health.UnresolvedIncidentsCount);
|
|
Xunit.Assert.False(health.IsFlawless);
|
|
}
|
|
|
|
[Fact]
|
|
public void 최대_치유_초과_시_최종_예외_전파()
|
|
{
|
|
var autoPilot = new SelfHealingAutoPilot();
|
|
|
|
Xunit.Assert.Throws<InvalidOperationException>(() =>
|
|
{
|
|
autoPilot.ExecuteWithSelfHealing<string>(
|
|
action: () => throw new InvalidOperationException("Permanent fatal hardware fault"),
|
|
diagnoseAndRepair: (_, _) => true,
|
|
maxHealingAttempts: 2
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 40개 마일스톤 전 영역 통합 5대 그랜드 연속 파이프라인 (Grand Unified Pipeline) 검증.
|
|
/// </summary>
|
|
public class GrandUnifiedPipelineMegaTests
|
|
{
|
|
[Fact]
|
|
public void 시나리오1_브라우징_보안_광고차단_메모리프리징_미디어탐색_파이프라인()
|
|
{
|
|
using var pipeline = GrandUnifiedPipeline.Create();
|
|
|
|
// 1. EasyList 광고 차단 규칙 로드
|
|
pipeline.AdBlockEngine.AddRule("||adservice.google.com^");
|
|
pipeline.AdBlockEngine.AddRule("||doubleclick.net^");
|
|
pipeline.AdBlockEngine.AddRule("@@||partner.ads.com/essential.js");
|
|
|
|
// 2. 다중 탭 생성 및 활동 추적
|
|
var tab1 = pipeline.TabManager.NewTab("https://site1.test/watch?v=123");
|
|
var tab2 = pipeline.TabManager.NewTab("https://site2.test/music");
|
|
var tab3 = pipeline.TabManager.NewTab("https://adservice.google.com/banner");
|
|
|
|
pipeline.MemoryGuard.TrackTabActivity(tab1.Id.ToString(), DateTime.UtcNow);
|
|
pipeline.MemoryGuard.TrackTabActivity(tab2.Id.ToString(), DateTime.UtcNow.AddMinutes(-20)); // 비활성 탭
|
|
|
|
// 3. 광고 요청 차단 확인
|
|
pipeline.SimulateWebRequest("https://adservice.google.com/banner", MediaKind.Direct);
|
|
Xunit.Assert.Empty(pipeline.DetectedItems); // 광고이므로 차단되어 미디어 미감지
|
|
|
|
// 4. 정상 미디어 요청 감지
|
|
pipeline.SimulateWebRequest("https://site1.test/video.m3u8", MediaKind.Hls);
|
|
Xunit.Assert.Single(pipeline.DetectedItems);
|
|
Xunit.Assert.Equal("https://site1.test/video.m3u8", pipeline.DetectedItems[0].Url);
|
|
|
|
// 5. 메모리 가디언 자동 프리징 확인
|
|
var toSuspend = pipeline.MemoryGuard.GetTabsToSuspend(DateTime.UtcNow);
|
|
Xunit.Assert.Contains(tab2.Id.ToString(), toSuspend);
|
|
}
|
|
|
|
[Fact]
|
|
public void 시나리오2_미디어분석_중복방지_디스크가디언_큐등록_파이프라인()
|
|
{
|
|
using var pipeline = GrandUnifiedPipeline.Create();
|
|
|
|
// 1. FFprobe JSON 메타데이터 정밀 분석
|
|
var sampleFfprobeJson = @"{
|
|
""streams"": [
|
|
{
|
|
""codec_type"": ""video"",
|
|
""codec_name"": ""h264"",
|
|
""width"": 1920,
|
|
""height"": 1080,
|
|
""r_frame_rate"": ""60/1""
|
|
},
|
|
{
|
|
""codec_type"": ""audio"",
|
|
""codec_name"": ""aac"",
|
|
""channels"": 2
|
|
}
|
|
],
|
|
""format"": { ""duration"": ""300.0"", ""size"": ""150000000"", ""bit_rate"": ""4000000"" }
|
|
}";
|
|
var report = MediaMetadataExtractor.ParseFfprobeOutput(sampleFfprobeJson);
|
|
Xunit.Assert.True(report.IsHd);
|
|
Xunit.Assert.Equal("1080p", report.ResolutionLabel);
|
|
|
|
// 2. pHash 중복 검출 엔진 등록 및 중복 방어
|
|
ulong originalHash = 0xAAAA_BBBB_CCCC_DDDD;
|
|
pipeline.Deduplicator.RegisterVideo("existing_video.mp4", originalHash);
|
|
|
|
ulong duplicateCandidate = 0xAAAA_BBBB_CCCC_DDDC; // 1비트 차이 (중복)
|
|
var match = pipeline.Deduplicator.FindDuplicate(duplicateCandidate);
|
|
Xunit.Assert.NotNull(match);
|
|
Xunit.Assert.Equal("existing_video.mp4", match.FilePath);
|
|
|
|
// 3. 디스크 잔여 용량 사전 점검
|
|
var hasDiskSpace = DiskSpaceGuardian.HasSufficientSpace(Path.GetTempPath(), report.FileSizeBytes);
|
|
Xunit.Assert.True(hasDiskSpace);
|
|
}
|
|
|
|
[Fact]
|
|
public void 시나리오3_적응형QoS_버퍼풀할당_카오스장애_지수백오프_자동복구_파이프라인()
|
|
{
|
|
using var pipeline = GrandUnifiedPipeline.Create();
|
|
|
|
// 1. 버퍼 풀 대여 및 I/O 준비
|
|
var buffer = BufferPoolManager.Rent(64 * 1024);
|
|
try
|
|
{
|
|
Xunit.Assert.NotNull(buffer);
|
|
|
|
// 2. 사용자 브라우징 상호작용 발생 시 QoS 대역폭 즉시 양보
|
|
pipeline.TrafficShaper.RecordUserActivity(DateTime.UtcNow);
|
|
var yieldBandwidth = pipeline.TrafficShaper.GetEffectiveBandwidthLimit(DateTime.UtcNow);
|
|
Xunit.Assert.True(yieldBandwidth <= 2_500_000); // 25% 이하로 양보
|
|
|
|
// 3. 유휴 5초 경과 시 풀스피드 회복
|
|
var restoredBandwidth = pipeline.TrafficShaper.GetEffectiveBandwidthLimit(DateTime.UtcNow.AddSeconds(5));
|
|
Xunit.Assert.Equal(10_000_000, restoredBandwidth);
|
|
|
|
// 4. 카오스 네트워크 장애 주입 시 지수 백오프 100% 복구
|
|
var result = pipeline.ChaosDefense.ExecuteWithChaosRecovery(() =>
|
|
{
|
|
if (pipeline.ChaosDefense.ShouldInjectFailure())
|
|
throw new HttpRequestException("Simulated transient socket drop");
|
|
return "100% Downloaded Stream Data";
|
|
}, maxRetries: 5);
|
|
|
|
Xunit.Assert.Equal("100% Downloaded Stream Data", result);
|
|
}
|
|
finally
|
|
{
|
|
BufferPoolManager.Return(buffer);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task 시나리오4_EBU_R128_오디오정규화_HDR톤매핑_챕터분할_파이프라인()
|
|
{
|
|
using var pipeline = GrandUnifiedPipeline.Create();
|
|
|
|
// 1. EBU R128 오디오 노멀라이제이션 (Loudnorm 2-pass)
|
|
var normSpec = LoudnessSpec.WebStandard;
|
|
var normalizedFile = await pipeline.AudioNormalizer.NormalizeAudioAsync("ffmpeg.exe", "input.mp4", "norm_output.mp4", normSpec, CancellationToken.None);
|
|
Xunit.Assert.Equal("norm_output.mp4", normalizedFile);
|
|
|
|
// 2. HDR10 색공간 감지 및 SDR 톤매핑
|
|
var isHdr = HdrToneMapper.IsHdrContent("bt2020nc", "smpte2084", "arib-std-b67");
|
|
Xunit.Assert.True(isHdr);
|
|
|
|
var sdrFile = await pipeline.HdrToneMapper.ConvertToSdrAsync("ffmpeg.exe", "hdr.mkv", "sdr.mp4", ToneMapAlgorithm.Mobius, CancellationToken.None);
|
|
Xunit.Assert.Equal("sdr.mp4", sdrFile);
|
|
|
|
// 3. 챕터 분할 및 FFMETADATA1 생성
|
|
var chapters = new[]
|
|
{
|
|
new ChapterInfo(1, "Intro", 0.0, 30.0),
|
|
new ChapterInfo(2, "Main Episode", 30.0, 180.0)
|
|
};
|
|
var splits = await pipeline.ChapterSplitter.SplitByChaptersAsync("ffmpeg.exe", "sdr.mp4", chapters, @"C:\output", CancellationToken.None);
|
|
Xunit.Assert.Equal(2, splits.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task 시나리오5_듀얼자막플레이어_P2P디스커버리_NAS클라우드동기화_파이프라인()
|
|
{
|
|
using var pipeline = GrandUnifiedPipeline.Create();
|
|
|
|
// 1. 내장 플레이어 주/보조 듀얼 자막 로드 및 오버레이
|
|
pipeline.DualSubtitleEngine.LoadPrimaryVtt("00:00:01.000 --> 00:00:05.000\nHello World");
|
|
pipeline.DualSubtitleEngine.LoadSecondaryVtt("00:00:01.000 --> 00:00:05.000\n안녕하세요 세계");
|
|
|
|
var dualCues = pipeline.DualSubtitleEngine.GetDualCues(3.0);
|
|
Xunit.Assert.NotNull(dualCues);
|
|
Xunit.Assert.Equal("Hello World", dualCues.PrimaryText);
|
|
Xunit.Assert.Equal("안녕하세요 세계", dualCues.SecondaryText);
|
|
|
|
// 2. mDNS 로컬 기기 디스커버리 광고 프로필 확인
|
|
var profile = MdnsAdvertiser.BuildProfile("My Desktop PC", 5000);
|
|
Xunit.Assert.Equal("_videodl._tcp", profile.ServiceName.ToString());
|
|
var srv = Xunit.Assert.Single(profile.Resources.OfType<Makaretu.Dns.SRVRecord>());
|
|
Xunit.Assert.Equal(5000, (int)srv.Port);
|
|
|
|
// 3. NAS / WebDAV / S3 클라우드 게이트웨이 백그라운드 자동 동기화
|
|
pipeline.CloudGateway.EnqueueSync(@"C:\output\video.mp4", "/nas/media/video.mp4");
|
|
var syncedCount = await pipeline.CloudGateway.ProcessAllPendingAsync(CancellationToken.None);
|
|
Xunit.Assert.Equal(1, syncedCount);
|
|
Xunit.Assert.Single(pipeline.CloudClient.UploadedList);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 100+ 대규모 감사 항목에 대한 하드닝 전수 검증 테스트 스위트
|
|
/// </summary>
|
|
public class GrandAuditHardeningTests
|
|
{
|
|
// ==========================================
|
|
// 1. FileNameUtil 하드닝 검증
|
|
// ==========================================
|
|
[Theory]
|
|
[InlineData("CON", "_CON")]
|
|
[InlineData("PRN", "_PRN")]
|
|
[InlineData("AUX", "_AUX")]
|
|
[InlineData("NUL", "_NUL")]
|
|
[InlineData("COM1", "_COM1")]
|
|
[InlineData("LPT2", "_LPT2")]
|
|
[InlineData("aux.mp4", "_aux.mp4")]
|
|
[InlineData("con.tar.gz", "_con.tar.gz")]
|
|
public void FileNameUtil_Windows_예약어를_안전하게_치환한다(string input, string expected)
|
|
{
|
|
var sanitized = FileNameUtil.Sanitize(input);
|
|
Assert.Equal(expected, sanitized);
|
|
}
|
|
|
|
[Fact]
|
|
public void FileNameUtil_서러게이트_페어_이모지_절단_시_인코딩_오류를_방지한다()
|
|
{
|
|
var text = "영상_이모지_테스트_" + "\uD83D\uDE00";
|
|
var sanitized = FileNameUtil.Sanitize(text, maxLen: 12);
|
|
Assert.False(char.IsHighSurrogate(sanitized[^1]));
|
|
}
|
|
|
|
[Fact]
|
|
public void FileNameUtil_상대_URI나_Data_URI에서도_크래시_없이_기본값을_반환한다()
|
|
{
|
|
var relUri = new Uri("/video/path/file.mp4", UriKind.Relative);
|
|
var name = FileNameUtil.DeriveName(relUri);
|
|
Assert.Equal("video", name);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("https://www.youtube.com/s/player/68c85eb0/failure", "www.youtube.com")]
|
|
[InlineData("https://example.com/api/v1/error", "example.com")]
|
|
[InlineData("https://example.com/video/stream/playback", "example.com")]
|
|
public void FileNameUtil_에러_및_제네릭_단어를_파일명으로_추출하지_않고_호스트명으로_폴백한다(string url, string expectedFallback)
|
|
{
|
|
var uri = new Uri(url);
|
|
var name = FileNameUtil.DeriveName(uri);
|
|
Assert.Equal(expectedFallback, name);
|
|
}
|
|
|
|
// ==========================================
|
|
// 2. M3u8Parser 하드닝 검증
|
|
// ==========================================
|
|
[Fact]
|
|
public void M3u8Parser_소문자_및_언더스코어_속성을_안전하게_파싱한다()
|
|
{
|
|
var m3u8 = """
|
|
#EXTM3U
|
|
#EXT-X-VERSION:3
|
|
#EXT-X-KEY:METHOD=AES-128,URI="https://test.com/key.bin",iv=0x1234567890abcdef1234567890abcdef,key_format="identity"
|
|
#EXTINF:10.0,
|
|
seg1.ts
|
|
""";
|
|
var parsed = M3u8Parser.Parse(m3u8, new Uri("https://test.com/playlist.m3u8"));
|
|
Assert.Single(parsed.Segments);
|
|
Assert.NotNull(parsed.Segments[0].Key);
|
|
Assert.Equal(new Uri("https://test.com/key.bin"), parsed.Segments[0].Key!.KeyUri);
|
|
}
|
|
|
|
[Fact]
|
|
public void M3u8Parser_특수문자나_공백이_포함된_세그먼트_URI를_안전하게_파싱한다()
|
|
{
|
|
var m3u8 = """
|
|
#EXTM3U
|
|
#EXTINF:5.0,
|
|
seg with spaces.ts
|
|
#EXTINF:5.0,
|
|
seg#invalid?.ts
|
|
""";
|
|
var parsed = M3u8Parser.Parse(m3u8, new Uri("https://test.com/master/play.m3u8"));
|
|
Assert.Equal(2, parsed.Segments.Count);
|
|
}
|
|
|
|
// ==========================================
|
|
// 3. DashParser 하드닝 검증
|
|
// ==========================================
|
|
[Fact]
|
|
public void DashParser_자릿수_패딩_포맷_식별자를_지원한다()
|
|
{
|
|
var mpd = """
|
|
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" mediaPresentationDuration="PT30S">
|
|
<Period>
|
|
<AdaptationSet mimeType="video/mp4">
|
|
<Representation id="v1" bandwidth="1000000">
|
|
<SegmentTemplate media="chunk-$Number%05d$.m4s" initialization="init.mp4" startNumber="1" duration="10" timescale="1"/>
|
|
</Representation>
|
|
</AdaptationSet>
|
|
</Period>
|
|
</MPD>
|
|
""";
|
|
var parsed = DashParser.Parse(mpd, new Uri("https://test.com/video.mpd"));
|
|
Assert.NotNull(parsed);
|
|
var video = parsed.Video;
|
|
Assert.NotNull(video);
|
|
Assert.Equal(3, video!.Segments.Count);
|
|
Assert.Equal(new Uri("https://test.com/chunk-00001.m4s"), video.Segments[0].Url);
|
|
Assert.Equal(new Uri("https://test.com/chunk-00002.m4s"), video.Segments[1].Url);
|
|
}
|
|
|
|
[Fact]
|
|
public void DashParser_일_단위가_포함된_ISO8601_기간을_정확히_파싱한다()
|
|
{
|
|
var mpd = """
|
|
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" mediaPresentationDuration="P1DT2H30M">
|
|
<Period>
|
|
<AdaptationSet mimeType="video/mp4">
|
|
<Representation id="v1" bandwidth="500000">
|
|
<BaseURL>video.mp4</BaseURL>
|
|
</Representation>
|
|
</AdaptationSet>
|
|
</Period>
|
|
</MPD>
|
|
""";
|
|
var parsed = DashParser.Parse(mpd, new Uri("https://test.com/dash.mpd"));
|
|
Assert.NotNull(parsed);
|
|
var video = parsed.Video;
|
|
Assert.NotNull(video);
|
|
Assert.Single(video!.Segments);
|
|
}
|
|
|
|
// ==========================================
|
|
// 4. MediaDetector & ProbeFormat 하드닝 검증
|
|
// ==========================================
|
|
[Theory]
|
|
[InlineData("https://cdn.example.com/live/playlist_1080p/seg_001.ts", true)]
|
|
[InlineData("https://cdn.example.com/master_stream/chunk_01.m4s", true)]
|
|
[InlineData("https://cdn.example.com/manifest/init.mp4", true)]
|
|
[InlineData("https://cdn.example.com/vod/master.m3u8", false)]
|
|
[InlineData("https://cdn.example.com/vod/playlist.m3u8", false)]
|
|
[InlineData("https://cdn.example.com/vod/manifest.mpd", false)]
|
|
public void MediaDetector_CDN_경로_내_매니페스트_키워드가_있어도_세그먼트를_정확히_판정한다(string url, bool expectedSegment)
|
|
{
|
|
Assert.Equal(expectedSegment, MediaDetector.IsSegment(url));
|
|
}
|
|
|
|
[Fact]
|
|
public void MediaDetector_ProbeFormat이_RIFF_AVI를_정상_식별한다()
|
|
{
|
|
byte[] aviHeader = new byte[]
|
|
{
|
|
0x52, 0x49, 0x46, 0x46,
|
|
0x00, 0x10, 0x00, 0x00,
|
|
0x41, 0x56, 0x49, 0x20
|
|
};
|
|
var format = MediaDetector.ProbeFormat(aviHeader);
|
|
Assert.Equal("avi", format);
|
|
}
|
|
|
|
// ==========================================
|
|
// 5. SocialSiteDetector 피싱 방어 검증
|
|
// ==========================================
|
|
[Theory]
|
|
[InlineData("https://fakeyoutube.com/watch?v=12345678901", false)]
|
|
[InlineData("https://phishing-instagram.com/reel/C12345", false)]
|
|
[InlineData("https://eviltiktok.com/@user/video/12345", false)]
|
|
[InlineData("https://www.youtube.com/watch?v=dQw4w9WgXcQ", true)]
|
|
[InlineData("https://youtu.be/dQw4w9WgXcQ", true)]
|
|
[InlineData("https://x.com/user/status/123456789", true)]
|
|
[InlineData("https://instagram.com/p/C_12345/", true)]
|
|
public void SocialSiteDetector_도메인_경계를_엄격히_검증하여_피싱을_차단한다(string url, bool expectedMatch)
|
|
{
|
|
var matched = SocialSiteDetector.TryMatch(new Uri(url), out var norm, out var label);
|
|
Assert.Equal(expectedMatch, matched);
|
|
}
|
|
|
|
// ==========================================
|
|
// 6. Server & PositionStore 원자적 저장 검증
|
|
// ==========================================
|
|
[Fact]
|
|
public void PositionStore_정상_및_비정상_위치값을_안전하게_저장_로드한다()
|
|
{
|
|
var tempDir = Path.Combine(Path.GetTempPath(), $"paca_pos_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempDir);
|
|
try
|
|
{
|
|
var store = new PositionStore(tempDir);
|
|
store.Set("video1", 123.45);
|
|
store.Set("video2", double.NaN);
|
|
store.Set("video3", -10.0);
|
|
|
|
Assert.Equal(123.45, store.Get("video1"));
|
|
Assert.Equal(0, store.Get("video2"));
|
|
Assert.Equal(0, store.Get("video3"));
|
|
Assert.Equal(0, store.Get("nonexistent"));
|
|
|
|
var store2 = new PositionStore(tempDir);
|
|
Assert.Equal(123.45, store2.Get("video1"));
|
|
}
|
|
finally
|
|
{
|
|
try { Directory.Delete(tempDir, true); } catch { }
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// 7. Mobile.Core OfflineStore 원자적 저장 검증
|
|
// ==========================================
|
|
[Fact]
|
|
public void OfflineStore_매니페스트_원자적_저장_및_삭제가_동작한다()
|
|
{
|
|
var tempDir = Path.Combine(Path.GetTempPath(), $"paca_off_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempDir);
|
|
try
|
|
{
|
|
var store = new OfflineStore(tempDir);
|
|
store.Record("item1", "test.mp4");
|
|
|
|
File.WriteAllText(store.PathFor("item1"), "content");
|
|
Assert.True(store.Exists("item1"));
|
|
Assert.Single(store.List());
|
|
|
|
store.Delete("item1");
|
|
Assert.False(store.Exists("item1"));
|
|
Assert.Empty(store.List());
|
|
}
|
|
finally
|
|
{
|
|
try { Directory.Delete(tempDir, true); } catch { }
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// 8. MdnsAdvertiser 한글 기기명 해시 접미사 검증
|
|
// ==========================================
|
|
[Fact]
|
|
public void MdnsAdvertiser_한글_기기명_입력시_충돌_없는_라벨을_생성한다()
|
|
{
|
|
var profile = MdnsAdvertiser.BuildProfile("내 컴퓨터", 5000);
|
|
Assert.NotNull(profile);
|
|
Assert.Contains("Paca-", profile.FullyQualifiedName?.ToString() ?? profile.Resources.FirstOrDefault()?.Name?.ToString() ?? "");
|
|
}
|
|
|
|
// ==========================================
|
|
// 9. ShortcutManager 키 파싱 확장 검증
|
|
// ==========================================
|
|
[Theory]
|
|
[InlineData("Ctrl+1", ModifierKeys.Control, Key.D1)]
|
|
[InlineData("Ctrl+9", ModifierKeys.Control, Key.D9)]
|
|
[InlineData("Ctrl+0", ModifierKeys.Control, Key.D0)]
|
|
[InlineData("Ctrl+Shift+P", ModifierKeys.Control | ModifierKeys.Shift, Key.P)]
|
|
[InlineData("Alt+Left", ModifierKeys.Alt, Key.Left)]
|
|
[InlineData("Ctrl++", ModifierKeys.Control, Key.OemPlus)]
|
|
[InlineData("Ctrl+-", ModifierKeys.Control, Key.OemMinus)]
|
|
public void ShortcutManager_숫자_기호_포함_단축키를_정확히_파싱한다(string gesture, ModifierKeys expectedMods, Key expectedKey)
|
|
{
|
|
var parsed = ShortcutManager.Parse(gesture);
|
|
Assert.NotNull(parsed);
|
|
Assert.Equal(expectedMods, parsed!.Value.Mods);
|
|
Assert.Equal(expectedKey, parsed.Value.Key);
|
|
}
|
|
|
|
// ==========================================
|
|
// 10. CommandPalette 동적 소스 예외 격리 검증
|
|
// ==========================================
|
|
[Fact]
|
|
public void CommandPalette_동적_제공자에서_예외가_발생해도_검색이_중단되지_않는다()
|
|
{
|
|
var palette = new CommandPalette();
|
|
palette.Register("정상 명령", "키워드", () => { });
|
|
palette.RegisterDynamic(() => throw new InvalidOperationException("동적 실패"));
|
|
|
|
var results = palette.Search("정상");
|
|
Assert.Single(results);
|
|
Assert.Equal("정상 명령", results[0].Title);
|
|
}
|
|
|
|
// ==========================================
|
|
// 12. Phase 66: 10,000-Gate Autonomous Chaos Matrix & Self-Healing
|
|
// ==========================================
|
|
[Fact]
|
|
public void ChaosFuzzingMatrixEngine_1000회_변이_인젝션_프로세스_크래시_0건_검증()
|
|
{
|
|
var report = Paca.Core.Infrastructure.ChaosFuzzingMatrixEngine.RunChaosFuzzingSuite(payload =>
|
|
{
|
|
// Simulate parser handling random byte streams
|
|
string str = System.Text.Encoding.UTF8.GetString(payload);
|
|
if (str.Contains("INVALID"))
|
|
throw new FormatException("Corrupted payload detected");
|
|
}, iterations: 1000);
|
|
|
|
Assert.Equal(1000, report.TotalMutationsGenerated);
|
|
Assert.Equal(0, report.UnhandledCrashesCount);
|
|
Assert.True(report.Passed);
|
|
}
|
|
|
|
[Fact]
|
|
public void AutonomousSelfHealingSystem_연속_장애_감지_및_자동_자가치유_검증()
|
|
{
|
|
var healing = new Paca.Core.Infrastructure.AutonomousSelfHealingSystem();
|
|
healing.RegisterService("HlsLiveEngine");
|
|
|
|
Assert.Equal(Paca.Core.Infrastructure.SubserviceHealthState.Healthy, healing.GetStatus("HlsLiveEngine").State);
|
|
|
|
// 1st & 2nd failure -> Degraded
|
|
healing.ReportFailure("HlsLiveEngine");
|
|
healing.ReportFailure("HlsLiveEngine");
|
|
Assert.Equal(Paca.Core.Infrastructure.SubserviceHealthState.Degraded, healing.GetStatus("HlsLiveEngine").State);
|
|
|
|
// 3rd failure -> Faulted
|
|
healing.ReportFailure("HlsLiveEngine");
|
|
Assert.Equal(Paca.Core.Infrastructure.SubserviceHealthState.Faulted, healing.GetStatus("HlsLiveEngine").State);
|
|
|
|
// Auto-heal recovery
|
|
bool restarted = false;
|
|
bool healed = healing.TryAutoHeal("HlsLiveEngine", () => restarted = true);
|
|
|
|
Assert.True(healed);
|
|
Assert.True(restarted);
|
|
Assert.Equal(Paca.Core.Infrastructure.SubserviceHealthState.Healthy, healing.GetStatus("HlsLiveEngine").State);
|
|
}
|
|
}
|
|
|
|
|