Implement QR pairing window and mDNS announcements; update documentation and tests
This commit is contained in:
@@ -44,6 +44,31 @@ catch (ArgumentException) { expiredInvitationRejected = true; }
|
||||
Check(expiredInvitationRejected, "expired pairing window cannot generate invitation");
|
||||
var descriptor = DiscoveryDescriptor.Create(new string('a', 64));
|
||||
Check(descriptor.AgentId == new string('A', 64) && descriptor.TxtRecords().Count == 2, "discovery descriptor stable and contains no pairing secrets");
|
||||
var qrPng = PairingQr.Render(invitation);
|
||||
using (var qrStream = new MemoryStream(qrPng))
|
||||
using (var bitmap = new Bitmap(qrStream))
|
||||
{
|
||||
var pixels = new byte[bitmap.Width * bitmap.Height * 3];
|
||||
for (var y = 0; y < bitmap.Height; y++)
|
||||
for (var x = 0; x < bitmap.Width; x++)
|
||||
{
|
||||
var color = bitmap.GetPixel(x, y);
|
||||
var offset = (y * bitmap.Width + x) * 3;
|
||||
pixels[offset] = color.R; pixels[offset + 1] = color.G; pixels[offset + 2] = color.B;
|
||||
}
|
||||
var reader = new ZXing.BarcodeReaderGeneric();
|
||||
var decodedQr = reader.Decode(pixels, bitmap.Width, bitmap.Height, ZXing.RGBLuminanceSource.BitmapFormat.RGB24);
|
||||
Check(decodedQr?.Text == invitation.ToQrPayload(), "generated QR decodes to exact pairing invitation");
|
||||
Check(bitmap.Width == bitmap.Height && bitmap.GetPixel(0, 0).R == 255, "QR is square with white quiet zone");
|
||||
}
|
||||
var serviceProfile = MdnsPublisher.CreateProfile(new string('A', 64), 18765, [IPAddress.Parse("192.168.1.10")]);
|
||||
Check(serviceProfile.Resources.OfType<Makaretu.Dns.SRVRecord>().Single().Port == 18765, "DNS-SD advertises actual HTTPS port");
|
||||
Check(serviceProfile.Resources.OfType<Makaretu.Dns.ARecord>().Single().Address.Equals(IPAddress.Parse("192.168.1.10")), "DNS-SD contains selected LAN address");
|
||||
Check(serviceProfile.Resources.OfType<Makaretu.Dns.TXTRecord>().Single().Strings.Contains("id=" + new string('A', 64)), "DNS-SD includes stable agent id");
|
||||
var invalidProfileRejected = false;
|
||||
try { MdnsPublisher.CreateProfile(new string('A', 64), 8765, [IPAddress.Loopback]); }
|
||||
catch (ArgumentException) { invalidProfileRejected = true; }
|
||||
Check(invalidProfileRejected, "mDNS cannot advertise loopback address");
|
||||
Check(TimelineMath.Position(20, 0, 200, true, now.AddSeconds(-5), now, 2) == 30, "position uses elapsed time and rate");
|
||||
Check(TimelineMath.Position(20, 0, 200, false, now.AddSeconds(-5), now, 1) == 20, "pause does not advance position");
|
||||
Check(TimelineMath.Position(199, 0, 200, true, now.AddSeconds(-5), now, 1) == 200, "position clamps to end");
|
||||
@@ -320,6 +345,63 @@ if (args.Contains("--live-read"))
|
||||
Check(next.ArtworkId == id, "real cover cache survives progress refresh");
|
||||
}
|
||||
}
|
||||
if (args.Contains("--qr-window-smoke"))
|
||||
{
|
||||
var uiClock = new TestClock(DateTimeOffset.UtcNow);
|
||||
var uiPairing = new PairingService(uiClock);
|
||||
uiPairing.Open();
|
||||
using var host = new MusicBridge.Agent.PairingWindowHost(uiPairing, new string('A', 64),
|
||||
() => [new Uri("https://192.168.1.10:8765")], visible: false);
|
||||
host.Open();
|
||||
var field = typeof(MusicBridge.Agent.PairingWindowHost).GetField("form", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
|
||||
Form? uiForm = null;
|
||||
using var uiTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||
while (uiForm is null || !uiForm.IsHandleCreated)
|
||||
{
|
||||
await Task.Delay(50, uiTimeout.Token);
|
||||
uiForm = field.GetValue(host) as Form;
|
||||
}
|
||||
var actualWindow = uiForm;
|
||||
bool HasImage() => (bool)actualWindow.Invoke(() => actualWindow.Controls.OfType<TableLayoutPanel>().Single()
|
||||
.Controls.OfType<PictureBox>().Single().Image is not null);
|
||||
while (!HasImage()) await Task.Delay(50, uiTimeout.Token);
|
||||
Check(HasImage(), "QR window renders active pairing image");
|
||||
actualWindow.Invoke(() =>
|
||||
{
|
||||
using var frame = new Bitmap(actualWindow.Width, actualWindow.Height);
|
||||
actualWindow.DrawToBitmap(frame, new Rectangle(0, 0, frame.Width, frame.Height));
|
||||
frame.Save(Path.Combine(AppContext.BaseDirectory, "qr-window-smoke.png"));
|
||||
});
|
||||
uiClock.Now = uiClock.Now.AddMinutes(6);
|
||||
while (HasImage()) await Task.Delay(50, uiTimeout.Token);
|
||||
Check(!HasImage(), "QR window removes expired pairing image");
|
||||
}
|
||||
if (args.Contains("--mdns-smoke"))
|
||||
{
|
||||
var local = LocalAddresses.Read();
|
||||
if (local.Length == 0) throw new InvalidOperationException("No private multicast-capable IPv4 interface for mDNS smoke test.");
|
||||
var testFingerprint = Convert.ToHexString(SHA256.HashData(Guid.NewGuid().ToByteArray()));
|
||||
var name = MdnsPublisher.CreateProfile(testFingerprint, 18799, local).FullyQualifiedName;
|
||||
using var listener = new Makaretu.Dns.MulticastService { UseIpv4 = true, UseIpv6 = false };
|
||||
var seen = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var gone = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
listener.AnswerReceived += (_, e) =>
|
||||
{
|
||||
foreach (var ptr in e.Message.Answers.OfType<Makaretu.Dns.PTRRecord>().Where(r => r.DomainName == name))
|
||||
{
|
||||
if (ptr.TTL == TimeSpan.Zero) gone.TrySetResult(true);
|
||||
else seen.TrySetResult(true);
|
||||
}
|
||||
};
|
||||
listener.Start();
|
||||
using (var publisher = new MdnsPublisher(testFingerprint, 18799))
|
||||
{
|
||||
publisher.Refresh(local);
|
||||
listener.SendQuery("_musicbridge._tcp.local", type: Makaretu.Dns.DnsType.PTR);
|
||||
Check(await seen.Task.WaitAsync(TimeSpan.FromSeconds(8)), "real multicast listener receives service announcement");
|
||||
}
|
||||
Check(await gone.Task.WaitAsync(TimeSpan.FromSeconds(8)), "real multicast listener receives goodbye on shutdown");
|
||||
}
|
||||
Console.WriteLine($"All {checks} checks passed. No real playback or volume was changed.");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user