releases/tests/Paca.Tests/Mobile/MobileLanAutoDiscoveryGateTests.cs

197 lines
7.1 KiB
C#

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;
/// <summary>
/// Gate 120 ~ 125: 원클릭 LAN 자동 발견 및 비콘 핸드셰이크 기계적 검증
/// </summary>
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);
}
}