162 lines
6.6 KiB
C#
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);
|
|
}
|
|
}
|