using MBN_STOCK_WEBVIEW.Playout.Interop; using MBN_STOCK_WEBVIEW.Playout.Registration; namespace MBN_STOCK_WEBVIEW.Playout.Tests; public sealed class K3dRegistrationProbeTests { private const string TypeLibraryPath = "C:\\vendor-secret\\typelib-x64.dll"; private const string EnginePath = "C:\\vendor-secret\\engine-x64.dll"; private const string EventHandlerPath = "C:\\vendor-secret\\events-x64.dll"; [Fact] public void Probe_WithReciprocalAmd64MachineRegistration_IsReady() { var binaries = ReadyBinaries(); var probe = CreateProbe(ReadySnapshot(), binaries); var report = probe.Probe(); Assert.True(report.IsReady); Assert.Equal(K3dRegistrationIssue.None, report.Issues); Assert.True(report.IsEngineClassReady); Assert.True(report.IsEventHandlerClassReady); Assert.Equal( new[] { TypeLibraryPath, EnginePath, EventHandlerPath }, binaries.InspectedPaths); } [Fact] public void Probe_WhenEitherDirectionOfReciprocalMappingDiffers_FailsClosed() { var snapshot = ReadySnapshot() with { Engine = ReadySnapshot().Engine with { ClassProgId = "Unexpected.Engine.1", ProgIdClassId = K3dComConstants.KaEventHandlerClassId }, EventHandler = ReadySnapshot().EventHandler with { ProgIdClassId = K3dComConstants.KaEngineClassId } }; var probe = CreateProbe(snapshot, ReadyBinaries()); var report = probe.Probe(); Assert.False(report.IsReady); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineProgIdMismatch)); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineProgIdClassIdMismatch)); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EventHandlerProgIdClassIdMismatch)); Assert.False(report.IsEngineClassReady); Assert.False(report.IsEventHandlerClassReady); AssertSafe(report); } [Fact] public void Probe_WhenEitherClassServerIsNotAmd64_FailsClosed() { var binaries = ReadyBinaries(); binaries.Set(EnginePath, exists: true, isAmd64: false); binaries.Set(EventHandlerPath, exists: true, isAmd64: false); var probe = CreateProbe(ReadySnapshot(), binaries); var report = probe.Probe(); Assert.False(report.IsReady); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineServerIsNotAmd64)); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EventHandlerServerIsNotAmd64)); Assert.False(report.IsEngineClassReady); Assert.False(report.IsEventHandlerClassReady); AssertSafe(report); } [Fact] public void Probe_WhenCurrentUserOverrideExists_FailsClosedWithoutDetails() { var probe = CreateProbe( ReadySnapshot() with { CurrentUserOverridePresent = true }, ReadyBinaries()); var report = probe.Probe(); Assert.False(report.IsReady); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.CurrentUserOverridePresent)); Assert.False(report.IsEngineClassReady); Assert.False(report.IsEventHandlerClassReady); Assert.Contains("재정의", report.Message, StringComparison.Ordinal); AssertSafe(report); } [Fact] public void CurrentUserOverrideBoundary_CoversBothClassesProgIdsAndTypeLibrary() { Assert.Equal( new[] { $"CLSID\\{K3dComConstants.KaEngineClassId}", $"CLSID\\{K3dComConstants.KaEventHandlerClassId}", K3dComConstants.KaEngineProgId, K3dComConstants.KaEventHandlerProgId, $"TypeLib\\{K3dComConstants.TypeLibraryId}" }, WindowsK3dRegistrySnapshotSource.CurrentUserOverridePaths); } [Fact] public void Probe_WhenRegistrySnapshotCannotBeRead_ReturnsSafeFailure() { var probe = new K3dRegistrationProbe( new ThrowingSnapshotSource(), ReadyBinaries()); var report = probe.Probe(); Assert.False(report.IsReady); Assert.Equal(K3dRegistrationIssue.RegistryReadFailed, report.Issues); AssertSafe(report); } [Fact] public void Probe_WhenProcessOrTypeLibraryIsNotX64_FailsClosed() { var snapshot = ReadySnapshot() with { Is64BitProcess = false, Win64TypeLibraryPath = null }; var probe = CreateProbe(snapshot, ReadyBinaries()); var report = probe.Probe(); Assert.False(report.IsReady); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.ProcessIsNot64Bit)); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.TypeLibraryMissing)); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.Win64BinaryMissing)); AssertSafe(report); } [Fact] public void Probe_WhenClassThreadingAndTypeLibraryDiffer_FailsClosed() { var snapshot = ReadySnapshot() with { Engine = ReadySnapshot().Engine with { ThreadingModel = "Both", TypeLibraryId = K3dComConstants.KaEventHandlerClassId }, EventHandler = ReadySnapshot().EventHandler with { ThreadingModel = "Free", TypeLibraryId = K3dComConstants.KaEngineClassId } }; var probe = CreateProbe(snapshot, ReadyBinaries()); var report = probe.Probe(); Assert.False(report.IsReady); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineThreadingModelMismatch)); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineTypeLibraryMismatch)); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EventHandlerThreadingModelMismatch)); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EventHandlerTypeLibraryMismatch)); Assert.False(report.IsEngineClassReady); Assert.False(report.IsEventHandlerClassReady); AssertSafe(report); } [Fact] public void Probe_WhenEitherClassRegistrationIsMissing_FailsClosed() { var snapshot = ReadySnapshot() with { Engine = ReadySnapshot().Engine with { ClassKeyPresent = false }, EventHandler = ReadySnapshot().EventHandler with { ClassKeyPresent = false } }; var probe = CreateProbe(snapshot, ReadyBinaries()); var report = probe.Probe(); Assert.False(report.IsReady); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineClassMissing)); Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EventHandlerClassMissing)); Assert.False(report.IsEngineClassReady); Assert.False(report.IsEventHandlerClassReady); AssertSafe(report); } private static K3dRegistrationProbe CreateProbe( K3dRegistrySnapshot snapshot, FakeBinaryInspector binaries) => new(new FakeSnapshotSource(snapshot), binaries); private static K3dRegistrySnapshot ReadySnapshot() => new( Is64BitProcess: true, Win64TypeLibraryPath: TypeLibraryPath, Engine: new K3dClassRegistrationSnapshot( ClassKeyPresent: true, ClassProgId: K3dComConstants.KaEngineProgId, ProgIdClassId: K3dComConstants.KaEngineClassId, InprocServerPath: EnginePath, ThreadingModel: K3dComConstants.ApartmentThreadingModel, TypeLibraryId: K3dComConstants.TypeLibraryId), EventHandler: new K3dClassRegistrationSnapshot( ClassKeyPresent: true, ClassProgId: K3dComConstants.KaEventHandlerProgId, ProgIdClassId: K3dComConstants.KaEventHandlerClassId, InprocServerPath: EventHandlerPath, ThreadingModel: K3dComConstants.ApartmentThreadingModel, TypeLibraryId: K3dComConstants.TypeLibraryId), CurrentUserOverridePresent: false); private static FakeBinaryInspector ReadyBinaries() => new FakeBinaryInspector() .Set(TypeLibraryPath, exists: true, isAmd64: true) .Set(EnginePath, exists: true, isAmd64: true) .Set(EventHandlerPath, exists: true, isAmd64: true); private static void AssertSafe(K3dRegistrationReport report) { Assert.DoesNotContain(TypeLibraryPath, report.Message, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain(EnginePath, report.Message, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain(EventHandlerPath, report.Message, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("HKEY", report.Message, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("CLSID\\", report.Message, StringComparison.OrdinalIgnoreCase); } private sealed class FakeSnapshotSource : IK3dRegistrySnapshotSource { private readonly K3dRegistrySnapshot _snapshot; public FakeSnapshotSource(K3dRegistrySnapshot snapshot) { _snapshot = snapshot; } public K3dRegistrySnapshot Capture() => _snapshot; } private sealed class ThrowingSnapshotSource : IK3dRegistrySnapshotSource { public K3dRegistrySnapshot Capture() => throw new UnauthorizedAccessException("HKEY secret registry detail"); } private sealed class FakeBinaryInspector : IPeBinaryInspector { private readonly Dictionary _inspections = new(StringComparer.OrdinalIgnoreCase); private readonly List _inspectedPaths = []; public IReadOnlyList InspectedPaths => _inspectedPaths; public FakeBinaryInspector Set(string path, bool exists, bool isAmd64) { _inspections[path] = new PeBinaryInspection(exists, isAmd64); return this; } public PeBinaryInspection Inspect(string? path) { if (path is null) { return new PeBinaryInspection(false, false); } _inspectedPaths.Add(path); return _inspections.TryGetValue(path, out var inspection) ? inspection : new PeBinaryInspection(false, false); } } }