feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization
This commit is contained in:
parent
a88aafa1e5
commit
0e568f1f0a
975 changed files with 130593 additions and 16783 deletions
|
|
@ -0,0 +1,162 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue