releases/tests/Paca.Tests/Platform/CrossPlatformParityHardeningPhase5Tests.cs

162 lines
6.6 KiB
C#

using System;
using System.IO;
using System.Linq;
using Paca.Avalonia;
using Paca.Mobile.Core;
using Xunit;
namespace Paca.Tests.Platform;
/// <summary>
/// Phase 5: 크로스플랫폼 패리티 및 모바일/데스크톱 견고성 헌법 게이트 (Gate 501 ~ 503).
/// - Gate 501: Paca.Avalonia MainViewModel 일시정지/재개(Pause/Resume) 및 IDisposable 생명주기
/// - Gate 502: Paca.Mobile.Core LanDiscoveryProber 널 안전성, IPv6 및 P2P QR TTL 만료 검증
/// - Gate 503: Paca.Mobile.Core SmartCollectionEngine 널 타이틀 방어 및 M3U8 개행문자 살균
/// </summary>
public class CrossPlatformParityHardeningPhase5Tests
{
[Fact]
public void Gate501_Avalonia_MainViewModel_Pause_Resume_And_Disposable_Lifecycle_Audit()
{
using var vm = new MainViewModel();
// 1. IDisposable 구현 검증
Assert.IsAssignableFrom<IDisposable>(vm);
// 2. PauseCommand & ResumeCommand 배선 확인
Assert.NotNull(vm.PauseCommand);
Assert.NotNull(vm.ResumeCommand);
Assert.NotNull(vm.DownloadCommand);
Assert.NotNull(vm.CancelCommand);
// 3. 초기 상태: IsBusy=false, IsPaused=false, CanExecute 검증
Assert.False(vm.IsBusy);
Assert.False(vm.IsPaused);
Assert.False(vm.PauseCommand.CanExecute(null));
Assert.False(vm.ResumeCommand.CanExecute(null));
// 4. IsBusy 상태 시뮬레이션
vm.IsBusy = true;
Assert.True(vm.PauseCommand.CanExecute(null));
Assert.False(vm.ResumeCommand.CanExecute(null));
// 5. 일시정지(Pause) 실행
vm.Pause();
Assert.True(vm.IsPaused);
Assert.Equal("일시정지됨", vm.Status);
Assert.False(vm.PauseCommand.CanExecute(null));
Assert.True(vm.ResumeCommand.CanExecute(null));
// 6. 재개(Resume) 실행
vm.Resume();
Assert.False(vm.IsPaused);
Assert.Equal("다운로드 재개 중...", vm.Status);
Assert.True(vm.PauseCommand.CanExecute(null));
Assert.False(vm.ResumeCommand.CanExecute(null));
// 7. 다운로드 완료(IsBusy=false) 시 IsPaused 자동 초기화
vm.Pause();
Assert.True(vm.IsPaused);
vm.IsBusy = false;
Assert.False(vm.IsPaused);
Assert.False(vm.PauseCommand.CanExecute(null));
Assert.False(vm.ResumeCommand.CanExecute(null));
}
[Fact]
public void Gate502_LanDiscoveryProber_Robust_Address_And_NullSafe_Packet_Handling_Audit()
{
var prober = new LanDiscoveryProber();
// 1. Null/Whitespace 페이로드 안전성 (예외 미발생)
prober.HandleReceivedPayload(null!, null!);
prober.HandleReceivedPayload("", "192.168.1.50");
prober.HandleReceivedPayload(" ", "");
// 2. RemoteIp가 null/빈값인 경우 127.0.0.1 기본 폴백
var pong = LanDiscoveryMessage.CreatePong("PC-NullIp", ":5000", true, "FP-001");
prober.HandleReceivedPayload(pong.ToPayload(), null!);
var devices = prober.GetActiveDevices();
var devNullIp = devices.FirstOrDefault(d => d.DeviceName == "PC-NullIp");
Assert.NotNull(devNullIp);
Assert.Equal("127.0.0.1:5000", devNullIp.Endpoint);
// 3. IPv6 remoteIp의 브래킷([]) 포맷팅 검증
var pongIpv6 = LanDiscoveryMessage.CreatePong("PC-IPv6", ":52100", true, "FP-IPV6");
prober.HandleReceivedPayload(pongIpv6.ToPayload(), "fe80::1");
devices = prober.GetActiveDevices();
var devIpv6 = devices.FirstOrDefault(d => d.DeviceName == "PC-IPv6");
Assert.NotNull(devIpv6);
Assert.Equal("[fe80::1]:52100", devIpv6.Endpoint);
Assert.Equal("http://[fe80::1]:52100", devIpv6.HttpBaseUrl);
// 4. P2pQrPayloadCodec TTL 만료 검증
var now = DateTimeOffset.UtcNow;
var freshPayload = new P2pQrPayload
{
Host = "192.168.1.10",
Port = 5000,
Token = "token-fresh",
CreatedAtUnix = now.ToUnixTimeSeconds()
};
var encodedFresh = P2pQrPayloadCodec.Encode(freshPayload);
Assert.Contains("&ts=", encodedFresh);
var decodedFresh = P2pQrPayloadCodec.Decode(encodedFresh);
Assert.Equal(freshPayload.CreatedAtUnix, decodedFresh.CreatedAtUnix);
Assert.False(P2pQrPayloadCodec.IsExpired(decodedFresh, TimeSpan.FromMinutes(5), now));
var expiredPayload = new P2pQrPayload
{
Host = "192.168.1.10",
Port = 5000,
Token = "token-expired",
CreatedAtUnix = now.AddMinutes(-10).ToUnixTimeSeconds()
};
var encodedExpired = P2pQrPayloadCodec.Encode(expiredPayload);
var decodedExpired = P2pQrPayloadCodec.Decode(encodedExpired);
Assert.True(P2pQrPayloadCodec.IsExpired(decodedExpired, TimeSpan.FromMinutes(5), now));
}
[Fact]
public void Gate503_SmartCollectionEngine_NullSafe_Title_And_M3u8_Sanitization_Audit()
{
// 1. Title이 null이거나 빈 문자열인 경우 NRE 없이 정상 분류
var itemNullTitle = new MediaItemMetadata(
Id: "item-1",
Title: null!,
Url: "https://youtube.com/shorts/test1234",
DurationSeconds: 45,
SizeBytes: 1024 * 1024,
Format: "mp4"
);
var itemAudio = new MediaItemMetadata(
Id: "item-2",
Title: "Podcast Episode 1",
Url: "https://example.com/audio.mp3",
DurationSeconds: 1200,
SizeBytes: 20 * 1024 * 1024,
Format: "mp3"
);
var itemWithNewlines = new MediaItemMetadata(
Id: "item-3",
Title: "Title With\r\nDangerous\nNewlines",
Url: "https://example.com/video.mp4",
DurationSeconds: 300,
SizeBytes: 50 * 1024 * 1024,
Format: "mp4"
);
var collections = SmartCollectionEngine.GroupIntoCollections(new[] { itemNullTitle, itemAudio, itemWithNewlines });
Assert.Contains(itemNullTitle, collections[SmartCollectionEngine.CollectionShorts]);
Assert.Contains(itemAudio, collections[SmartCollectionEngine.CollectionMusic]);
Assert.Contains(itemWithNewlines, collections[SmartCollectionEngine.CollectionGeneral]);
// 2. M3U8 플레이리스트 생성 시 개행 문자 살균 검증
var m3u8 = SmartCollectionEngine.GenerateM3u8Playlist("Playlist\r\nName", new[] { itemWithNewlines });
Assert.DoesNotContain("\r\nDangerous", m3u8);
Assert.Contains("#PLAYLIST:Playlist Name", m3u8);
Assert.Contains("#EXTINF:300,Title With Dangerous Newlines", m3u8);
}
}