using System.IO; using FluentAssertions; using TeamsISO.App.Services; using TeamsISO.App.Tests.Fakes; namespace TeamsISO.App.Tests.Services; // Tests for the OscBridge.DispatchAsync routing. We construct // OscMessage instances directly (skipping the UDP receive loop) and // assert that the right address resolves to the right controller call. // // The toggle / preset paths require Application.Current.Dispatcher, // which doesn't exist in xUnit's default execution context — those // paths return early on the null check, so we verify the bail rather // than the happy path. The full toggle path is covered in branch 11's // integration test that boots a real dispatcher. public sealed class OscBridgeDispatchTests : IDisposable { private readonly string _tempNotesDir; private readonly string? _previousNotesOverride; public OscBridgeDispatchTests() { _tempNotesDir = Path.Combine(Path.GetTempPath(), $"teamsiso-osc-{Guid.NewGuid():N}"); _previousNotesOverride = NotesService.DirectoryOverride; NotesService.DirectoryOverride = _tempNotesDir; } public void Dispose() { NotesService.DirectoryOverride = _previousNotesOverride; try { if (Directory.Exists(_tempNotesDir)) Directory.Delete(_tempNotesDir, recursive: true); } catch { /* best-effort */ } } private static (OscBridge Bridge, StubIsoController Controller) NewBridge() { var controller = new StubIsoController(); // OscBridge takes Func — returning null exercises // the "no VM yet" graceful path in handlers that need it. var bridge = new OscBridge(controller, () => null, logger: null); return (bridge, controller); } [Fact] public async Task RefreshDiscoveryAddress_CallsControllerRefreshDiscovery() { var (bridge, controller) = NewBridge(); await bridge.DispatchAsync(new OscMessage { Address = "/teamsiso/refresh-discovery" }); controller.RefreshDiscoveryCalled.Should().BeTrue(); } [Fact] public async Task UnknownAddress_NoOpsCleanly() { var (bridge, controller) = NewBridge(); await bridge.DispatchAsync(new OscMessage { Address = "/teamsiso/nope/never" }); controller.RefreshDiscoveryCalled.Should().BeFalse(); controller.EnableCalls.Should().BeEmpty(); controller.DisableCalls.Should().BeEmpty(); } [Fact] public async Task NotesAddress_AppendsViaNotesService() { var (bridge, _) = NewBridge(); await bridge.DispatchAsync(new OscMessage { Address = "/teamsiso/notes", TypeTag = ",s", Args = new object[] { "tracked through OSC" }, }); File.Exists(NotesService.TodayPath).Should().BeTrue(); File.ReadAllText(NotesService.TodayPath).Should().Contain("tracked through OSC"); } [Fact] public async Task StopAllAddress_NoOpsWhenViewModelIsNull() { // Without a view-model, the stop-all path returns before touching // the controller. The point of this test is to pin that the bail // is clean — no thrown exception, no controller traffic. var (bridge, controller) = NewBridge(); await bridge.DispatchAsync(new OscMessage { Address = "/teamsiso/stop-all" }); controller.DisableCalls.Should().BeEmpty(); } [Fact] public async Task IsoByNameAddress_NoOpsWhenViewModelIsNull() { // /teamsiso/iso "Jane" 1 — verifies the bail when no VM is // wired; doesn't fire EnableIsoAsync. The dispatcher-equipped // version of this round-trip lives in branch 11. var (bridge, controller) = NewBridge(); await bridge.DispatchAsync(new OscMessage { Address = "/teamsiso/iso", TypeTag = ",sT", Args = new object[] { "Jane", true }, }); controller.EnableCalls.Should().BeEmpty(); } }