using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Paca.Mobile.Core; using Xunit; namespace Paca.Tests.Mobile; /// /// Gate 120 ~ 125: 원클릭 LAN 자동 발견 및 비콘 핸드셰이크 기계적 검증 /// public class MobileLanAutoDiscoveryGateTests { [Fact] public void Gate120_BeaconMessage_Serialization_And_Parsing() { var msg = LanDiscoveryMessage.CreatePong( deviceName: "Desktop-Encep", endpoint: "192.168.0.10:52100", pairingMode: true, fingerprint: "PACA-TEST-FP" ); var encoded = msg.ToPayload(); Assert.StartsWith("PACA_DISCOVERY|PONG|", encoded); var parsed = LanDiscoveryMessage.Parse(encoded); Assert.NotNull(parsed); Assert.Equal(LanDiscoveryMessageType.Pong, parsed.Type); Assert.Equal("Desktop-Encep", parsed.DeviceName); Assert.Equal("192.168.0.10:52100", parsed.Endpoint); Assert.True(parsed.IsPairingAllowed); Assert.Equal("PACA-TEST-FP", parsed.Fingerprint); } [Fact] public void Gate121_PingMessage_Creation() { var ping = LanDiscoveryMessage.CreatePing(clientDeviceName: "Galaxy-S24"); var payload = ping.ToPayload(); Assert.Equal("PACA_DISCOVERY|PING|Galaxy-S24", payload); var parsed = LanDiscoveryMessage.Parse(payload); Assert.NotNull(parsed); Assert.Equal(LanDiscoveryMessageType.Ping, parsed.Type); Assert.Equal("Galaxy-S24", parsed.DeviceName); } [Fact] public void Gate122_Invalid_Or_Malformed_Beacon_Handled_Safely() { Assert.Null(LanDiscoveryMessage.Parse(null!)); Assert.Null(LanDiscoveryMessage.Parse("")); Assert.Null(LanDiscoveryMessage.Parse("RANDOM_PACKET|TEST")); Assert.Null(LanDiscoveryMessage.Parse("PACA_DISCOVERY|UNKNOWN")); } [Fact] public void Gate123_LanDiscoveredDevice_Formatting() { var device = new DiscoveredLanDevice( DeviceName: "My-Studio-PC", Endpoint: "192.168.1.50:52100", IsPairingAllowed: true, Fingerprint: "FP123", LastSeen: DateTimeOffset.UtcNow ); Assert.Equal("http://192.168.1.50:52100", device.HttpBaseUrl); Assert.True(device.IsAlive(TimeSpan.FromSeconds(5))); } [Fact] public async Task Gate124_Mock_Prober_Discovers_Responding_Host() { var prober = new LanDiscoveryProber(); var fakePong = LanDiscoveryMessage.CreatePong("Encep-PC", "192.168.0.2:52100", true, "FP-001"); // 시뮬레이션 패킷 수신 처리 prober.HandleReceivedPayload(fakePong.ToPayload(), "192.168.0.2"); var devices = prober.GetActiveDevices(); Assert.Single(devices); Assert.Equal("Encep-PC", devices[0].DeviceName); Assert.Equal("192.168.0.2:52100", devices[0].Endpoint); Assert.True(devices[0].IsPairingAllowed); } [Fact] public void Gate125_Prober_Removes_Stale_Devices() { var prober = new LanDiscoveryProber(); var pong = LanDiscoveryMessage.CreatePong("Old-PC", "192.168.0.99:52100", false, "FP-OLD"); // 10초 전 시점으로 등록 prober.AddOrUpdate(new DiscoveredLanDevice( DeviceName: pong.DeviceName, Endpoint: pong.Endpoint, IsPairingAllowed: pong.IsPairingAllowed, Fingerprint: pong.Fingerprint, LastSeen: DateTimeOffset.UtcNow.AddSeconds(-15) )); prober.PruneStale(maxAge: TimeSpan.FromSeconds(10)); var devices = prober.GetActiveDevices(); Assert.Empty(devices); } [Fact] public async Task Gate126_LanBeaconResponder_Responds_To_Ping_Correctly() { using var responder = new Paca.Server.Discovery.LanBeaconResponder( deviceName: "Desktop-Workstation", httpPort: 52100, pairingAllowed: true, fingerprint: "DESK-FP-999" ); // 로컬 비콘 프로필 검증 var pong = responder.BuildPongMessage("192.168.1.20"); Assert.NotNull(pong); Assert.Equal("Desktop-Workstation", pong.DeviceName); Assert.Equal("192.168.1.20:52100", pong.Endpoint); Assert.True(pong.IsPairingAllowed); Assert.Equal("DESK-FP-999", pong.Fingerprint); // PING 패킷 처리 검증 var pingPayload = LanDiscoveryMessage.CreatePing("Pixel-9").ToPayload(); var replyPayload = responder.ProcessIncomingPayload(pingPayload, "192.168.1.20"); Assert.NotNull(replyPayload); var parsed = LanDiscoveryMessage.Parse(replyPayload); Assert.NotNull(parsed); Assert.Equal(LanDiscoveryMessageType.Pong, parsed.Type); Assert.Equal("Desktop-Workstation", parsed.DeviceName); } [Fact] public void Gate127_LanBeaconResponder_Does_Not_Echo_SenderIp_As_Server_Endpoint_Audit() { using var responder = new Paca.Server.Discovery.LanBeaconResponder( deviceName: "Desktop-Studio", httpPort: 5000, pairingAllowed: true, fingerprint: "DESK-FP-1" ); // 모바일 클라이언트 IP가 192.168.1.88인 경우 var pingPayload = LanDiscoveryMessage.CreatePing("Galaxy-Phone").ToPayload(); var replyPayload = responder.ProcessIncomingPayload(pingPayload, "192.168.1.88"); Assert.NotNull(replyPayload); var parsed = LanDiscoveryMessage.Parse(replyPayload); Assert.NotNull(parsed); // PONG 엔드포인트는 송신자(모바일)의 IP(192.168.1.88)가 아니라 0.0.0.0 또는 서버 바인딩이어야 함 Assert.NotEqual("192.168.1.88:5000", parsed.Endpoint); Assert.Equal("0.0.0.0:5000", parsed.Endpoint); // 모바일 프로버가 수신했을 때, 패킷을 보낸 데스크톱 IP(예: 192.168.1.10)로 올바르게 복원되는지 검증 var prober = new LanDiscoveryProber(); prober.HandleReceivedPayload(replyPayload, "192.168.1.10"); var devices = prober.GetActiveDevices(); Assert.Single(devices); Assert.Equal("192.168.1.10:5000", devices[0].Endpoint); Assert.Equal("http://192.168.1.10:5000", devices[0].HttpBaseUrl); } [Fact] public void Gate128_LanDiscoveryProber_Resolves_0_0_0_0_And_ColonPort_To_RemoteIp_Audit() { var prober = new LanDiscoveryProber(); // 1) 0.0.0.0:5000 형태 var pongZero = LanDiscoveryMessage.CreatePong("PC-A", "0.0.0.0:5000", true, "FP-A"); prober.HandleReceivedPayload(pongZero.ToPayload(), "10.0.0.5"); // 2) :5000 형태 (IP 생략 콜론 포트) var pongColon = LanDiscoveryMessage.CreatePong("PC-B", ":52100", true, "FP-B"); prober.HandleReceivedPayload(pongColon.ToPayload(), "10.0.0.6"); var devices = prober.GetActiveDevices(); Assert.Equal(2, devices.Count); var devA = devices.First(d => d.DeviceName == "PC-A"); Assert.Equal("10.0.0.5:5000", devA.Endpoint); var devB = devices.First(d => d.DeviceName == "PC-B"); Assert.Equal("10.0.0.6:52100", devB.Endpoint); } }