using System; using System.IO; using System.Threading.Tasks; using Paca.Avalonia; using Paca.Mobile.Core; using Xunit; namespace Paca.Tests.Platform; public class AvaloniaAndMobileDiscoveryGateTests { private static string SolutionRoot { get { var dir = AppContext.BaseDirectory; while (!string.IsNullOrEmpty(dir) && !File.Exists(Path.Combine(dir, "Paca.slnx")) && !File.Exists(Path.Combine(dir, "Paca.sln"))) { var parent = Directory.GetParent(dir); if (parent == null) break; dir = parent.FullName; } return dir ?? AppContext.BaseDirectory; } } // ========================================================================= // Gate 1: Avalonia MainViewModel CTS 리소스 누수 방지 검증 // ========================================================================= [Fact] public async Task Gate_Avalonia_MainViewModel_Cts_Is_Disposed_And_Nulled_After_Execution() { var vm = new MainViewModel(); vm.Url = "https://example.com/not-supported"; // 즉시 실패/완료 유도 await vm.RunDownloadAsync(); Assert.False(vm.IsBusy); Assert.False(vm.HasActiveCts, "다운로드 완료 또는 실패 후 CTS는 Dispose되고 null 처리되어야 합니다."); } // ========================================================================= // Gate 2: Avalonia MainViewModel Log 컬렉션 슬라이딩 윈도우 상한(200개) 검증 // ========================================================================= [Fact] public void Gate_Avalonia_MainViewModel_Log_Sliding_Window_Limit() { var vm = new MainViewModel(); for (int i = 0; i < 300; i++) { vm.AddLogEntry($"Log item {i}"); } Assert.True(vm.Log.Count <= 200, $"Log 항목 수는 최대 200개 이하로 캡핑되어야 합니다. 현재: {vm.Log.Count}"); } // ========================================================================= // Gate 3: Avalonia MainWindow.axaml 바인딩 무결성 (CompileBindings 및 WPF 잔재 제거) // ========================================================================= [Fact] public void Gate_Avalonia_MainWindow_Axaml_Clean_Bindings() { var axamlPath = Path.Combine(SolutionRoot, "src", "Paca.Avalonia", "MainWindow.axaml"); Assert.True(File.Exists(axamlPath), $"MainWindow.axaml must exist at {axamlPath}"); var content = File.ReadAllText(axamlPath); Assert.DoesNotContain("x:CompileBindings=\"False\"", content); Assert.DoesNotContain("UpdateSourceTrigger=PropertyChanged", content); } // ========================================================================= // Gate 4: LanDiscoveryMessage 디바이스명에 파이프(|) 포함 시 파싱 무결성 검증 // ========================================================================= [Fact] public void Gate_LanDiscoveryMessage_Parses_DeviceName_With_Pipes() { var devNameWithPipes = "My|Studio|PC"; var endpoint = "192.168.1.100:5000"; var rawPayload = $"PACA_DISCOVERY|PONG|{devNameWithPipes}|{endpoint}|1|abc123fingerprint"; var parsed = LanDiscoveryMessage.Parse(rawPayload); Assert.NotNull(parsed); Assert.Equal(LanDiscoveryMessageType.Pong, parsed.Type); Assert.Equal(devNameWithPipes, parsed.DeviceName); Assert.Equal(endpoint, parsed.Endpoint); Assert.True(parsed.IsPairingAllowed); Assert.Equal("abc123fingerprint", parsed.Fingerprint); } // ========================================================================= // Gate 5: LanDiscoveryProber IPv6 엔드포인트 브래킷 포맷팅 검증 // ========================================================================= [Fact] public void Gate_LanDiscoveryProber_Formats_IPv6_Endpoint_With_Brackets() { using var prober = new LanDiscoveryProber(); DiscoveredLanDevice? foundDevice = null; prober.OnDeviceFound += d => foundDevice = d; // 원격 송신자가 0.0.0.0:5000을 보냈고, 송신자 IP가 IPv6인 경우 var ipv6Sender = "fe80::1"; prober.HandleReceivedPayload("PACA_DISCOVERY|PONG|TestNode|0.0.0.0:5000|1|fp", ipv6Sender); Assert.NotNull(foundDevice); Assert.Contains("[fe80::1]:5000", foundDevice.Endpoint); Assert.Equal("http://[fe80::1]:5000", foundDevice.HttpBaseUrl); } }