using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using BizTalkPlatformManagementTool.Models; using BizTalkPlatformManagementTool.Services; using BizTalkPlatformManagementTool.Setup; namespace BizTalkPlatformManagementTool.Tests { /// /// Enthält die portable Regressionstestsuite ohne Abhängigkeit von einem externen Testframework. /// internal static class Program { /// Anzahl der im aktuellen Testlauf fehlgeschlagenen Prüfungen. private static int failures; /// Führt alle Regressionstests aus und liefert einen CI-tauglichen Exitcode. /// Null, wenn alle Tests bestanden wurden; andernfalls eins. private static int Main() { Run("JsonRoundTripIsBomTolerantAndAtomic", JsonRoundTripIsBomTolerantAndAtomic); Run("DiffUsesApplicationAndNameIdentity", DiffUsesApplicationAndNameIdentity); Run("RestoreRejectsDifferentServer", RestoreRejectsDifferentServer); Run("Legacy213SnapshotIsAcceptedForRecovery", Legacy213SnapshotIsAcceptedForRecovery); Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder); Run("ShutdownPlanUsesGlobalSafeOrder", ShutdownPlanUsesGlobalSafeOrder); Run("ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure", ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure); Run("EmergencyRestorePlanStartsSsoFirst", EmergencyRestorePlanStartsSsoFirst); Run("HostInstancePlanAcceptsShortAndFqdnServer", HostInstancePlanAcceptsShortAndFqdnServer); Run("PlanExecutionContinuesAfterSchedulerFailure", PlanExecutionContinuesAfterSchedulerFailure); Run("PlanExecutionContinuesAfterMultipleUnexpectedExceptions", PlanExecutionContinuesAfterMultipleUnexpectedExceptions); Run("LoggerSinkFailureCannotAbortPlanExecution", LoggerSinkFailureCannotAbortPlanExecution); Run("PlanExecutionSkipsAlreadySatisfiedState", PlanExecutionSkipsAlreadySatisfiedState); Run("ExecutionReportRoundTripPreservesFailure", ExecutionReportRoundTripPreservesFailure); Run("ScheduledReceivePlanCarriesAdapterMetadata", ScheduledReceivePlanCarriesAdapterMetadata); Run("AdapterAssemblyResolverRequiresMatchingIdentity", AdapterAssemblyResolverRequiresMatchingIdentity); Run("OperationLogPersistsCompressesAndRetainsThirtyDays", OperationLogPersistsCompressesAndRetainsThirtyDays); Run("OperationLogUsesVerifiedStartupFallback", OperationLogUsesVerifiedStartupFallback); Run("OperationLogFailsOverAfterAppendFailure", OperationLogFailsOverAfterAppendFailure); Run("OperationLogSurfacesTotalStorageFailure", OperationLogSurfacesTotalStorageFailure); Run("ExceptionDiagnosticsContainSupportContext", ExceptionDiagnosticsContainSupportContext); Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues); Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering); Run("PackageManifestRejectsUndeclaredAndTraversalFiles", PackageManifestRejectsUndeclaredAndTraversalFiles); Run("InstallerActivatesValidatedPayload", InstallerActivatesValidatedPayload); Run("InstallerRetriesTransientActivationMove", InstallerRetriesTransientActivationMove); Run("InstallerUsesVerifiedCopyFallbackForNewInstall", InstallerUsesVerifiedCopyFallbackForNewInstall); Run("InstallerUsesVerifiedCopyFallbackForBackedUpUpdate", InstallerUsesVerifiedCopyFallbackForBackedUpUpdate); Run("InstallerRollsBackFailedUpdateCopyFallback", InstallerRollsBackFailedUpdateCopyFallback); Run("InstallerStopsAfterBoundedUpdateMoveRetries", InstallerStopsAfterBoundedUpdateMoveRetries); Run("InstallerDoesNotMutateOnStagingFailure", InstallerDoesNotMutateOnStagingFailure); Run("InstallerRollsBackFailedActivatedSelfTest", InstallerRollsBackFailedActivatedSelfTest); Run("InstallerUninstallRemovesProgramDirectory", InstallerUninstallRemovesProgramDirectory); Run("InstallerDiagnosticLogContainsContextAndExceptionChain", InstallerDiagnosticLogContainsContextAndExceptionChain); Run("InstallerDiagnosticLogFallsBackToTemp", InstallerDiagnosticLogFallsBackToTemp); Run("InstallerSurvivesUiReportFailure", InstallerSurvivesUiReportFailure); Run("OptionalWindowsIntegrationFailuresAreNonFatal", OptionalWindowsIntegrationFailuresAreNonFatal); Console.WriteLine(failures == 0 ? "ALL TESTS PASSED" : failures + " TEST(S) FAILED"); return failures == 0 ? 0 : 1; } /// Führt einen einzelnen Test isoliert aus und protokolliert sein Ergebnis. /// Der stabile Testname für die Konsolenausgabe. /// Die auszuführende Testfunktion. private static void Run(string name, Action test) { try { test(); Console.WriteLine("PASS " + name); } catch (Exception ex) { failures++; Console.Error.WriteLine("FAIL " + name + ": " + ex); } } /// Prüft BOM-tolerantes Lesen und rückstandsfreies atomisches JSON-Schreiben. private static void JsonRoundTripIsBomTolerantAndAtomic() { InTemp(directory => { var path = Path.Combine(directory, "snapshot.json"); var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted); JsonFileStore.Save(path, snapshot); JsonFileStore.Save(path, snapshot); var original = File.ReadAllBytes(path); var withBom = new byte[original.Length + 3]; withBom[0] = 0xef; withBom[1] = 0xbb; withBom[2] = 0xbf; Buffer.BlockCopy(original, 0, withBom, 3, original.Length); File.WriteAllBytes(path, withBom); Assert(JsonFileStore.Load(path).Applications.Count == 1, "BOM JSON did not load"); Assert(Directory.GetFiles(directory, "*.tmp.*").Length == 0, "temporary files remained"); Assert(Directory.GetFiles(directory, "*.bak.*").Length == 0, "backup files remained"); }); } /// Prüft, dass gleichnamige Artefakte verschiedener Anwendungen getrennt verglichen werden. private static void DiffUsesApplicationAndNameIdentity() { var before = Snapshot("APP-A", "SHARED", ArtifactStates.SendPortStarted); before.Applications.Add(Snapshot("APP-B", "SHARED", ArtifactStates.SendPortStarted).Applications[0]); var after = Snapshot("APP-A", "SHARED", ArtifactStates.SendPortStopped); after.Applications.Add(Snapshot("APP-B", "SHARED", ArtifactStates.SendPortStarted).Applications[0]); var diff = SnapshotComparer.Compare(before, after); Assert(diff.ArtifactDifferences.Count == 1, "expected one application-scoped difference"); Assert(diff.ArtifactDifferences[0].Application == "APP-A", "wrong application was compared"); } /// Prüft Kurzname/FQDN-Kompatibilität und Ablehnung eines fremden Restore-Zielservers. private static void RestoreRejectsDifferentServer() { var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted); snapshot.Server = "BIZTALK-A.example.local"; SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-A"); Expect(() => SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-B")); } /// Prüft, dass ein erhaltener 2.1.3-Snapshot ohne Versionssperre als Recovery-Quelle dient. private static void Legacy213SnapshotIsAcceptedForRecovery() { InTemp(directory => { var original = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted); original.ToolVersion = "2.1.3-net461"; original.Server = "BIZTALK-A.example.local"; var path = Path.Combine(directory, "before.json"); JsonFileStore.Save(path, original); var loaded = JsonFileStore.Load(path); SnapshotValidator.EnsureServerMatches(loaded, "BIZTALK-A"); var plan = new BizTalkOperationService(null).CreateEmergencyRestorePlan(loaded, "BIZTALK-A"); Assert(plan.Steps.First().Kind == "WindowsService", "legacy snapshot did not create an emergency plan"); Assert(loaded.ToolVersion == "2.1.3-net461", "legacy tool version was not preserved"); }); } /// Prüft die sichere Restore-Reihenfolge und den Schutz gebundener Orchestrierungen. private static void RestorePlanUsesSafeOrder() { var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted); snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HOST:SERVER", HostName = "HOST", Server = snapshot.Server, RawState = ArtifactStates.HostStarted }); snapshot.Applications[0].Orchestrations.Add(new OrchestrationState { Application = "APP", Name = "ORCH", OrchestrationStatus = ArtifactStates.OrchestrationBound }); snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RL", Enabled = true }); var second = Snapshot("APP-B", "PORT-B", ArtifactStates.SendPortStarted).Applications[0]; second.Orchestrations.Add(new OrchestrationState { Application = "APP-B", Name = "ORCH-B", OrchestrationStatus = ArtifactStates.OrchestrationStarted }); second.ReceiveLocations.Add(new ReceiveLocationState { Application = "APP-B", Name = "RL-B", Enabled = true }); snapshot.Applications.Add(second); var plan = new BizTalkOperationService(null).CreateRestorePlan(snapshot, snapshot.Server); Assert(plan.Steps.First().Kind == "HostInstance", "host instance must start first"); var firstReceiveLocation = plan.Steps.FindIndex(x => x.Kind == "ReceiveLocation"); var lastSendPort = plan.Steps.FindLastIndex(x => x.Kind == "SendPort"); var lastOrchestration = plan.Steps.FindLastIndex(x => x.Kind == "Orchestration" || x.Kind == "Note"); Assert(firstReceiveLocation > lastSendPort && firstReceiveLocation > lastOrchestration, "all receive locations must be restored after all send ports and orchestrations"); var bound = plan.Steps.Single(x => x.Name == "ORCH"); Assert(!bound.Execute && bound.Kind == "Note", "bound orchestration must remain unchanged"); } /// Prüft die globale Shutdown-Reihenfolge auch über mehrere Anwendungen hinweg. private static void ShutdownPlanUsesGlobalSafeOrder() { var snapshot = Snapshot("APP-A", "SP-A", ArtifactStates.SendPortStarted); snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP-A", Name = "RL-A", Enabled = true }); snapshot.Applications[0].Orchestrations.Add(new OrchestrationState { Application = "APP-A", Name = "ORCH-A", OrchestrationStatus = ArtifactStates.OrchestrationStarted }); var appB = Snapshot("APP-B", "SP-B", ArtifactStates.SendPortStarted).Applications[0]; appB.ReceiveLocations.Add(new ReceiveLocationState { Application = "APP-B", Name = "RL-B", Enabled = true }); appB.Orchestrations.Add(new OrchestrationState { Application = "APP-B", Name = "ORCH-B", OrchestrationStatus = ArtifactStates.OrchestrationStarted }); snapshot.Applications.Add(appB); snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HOST:SERVER", HostName = "HOST", Server = snapshot.Server, RawState = ArtifactStates.HostStarted }); var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server); var lastReceiveLocation = plan.Steps.FindLastIndex(x => x.Kind == "ReceiveLocation"); var firstOrchestration = plan.Steps.FindIndex(x => x.Kind == "Orchestration"); var lastOrchestration = plan.Steps.FindLastIndex(x => x.Kind == "Orchestration"); var firstSendPort = plan.Steps.FindIndex(x => x.Kind == "SendPort"); var lastSendPort = plan.Steps.FindLastIndex(x => x.Kind == "SendPort"); var firstHost = plan.Steps.FindIndex(x => x.Kind == "HostInstance"); Assert(lastReceiveLocation < firstOrchestration, "receive locations were not globally first"); Assert(lastOrchestration < firstSendPort, "orchestrations were not globally before send ports"); Assert(lastSendPort < firstHost, "host instances were not globally last"); } /// Prüft, dass Scheduler-Erkennung auch nach Planpersistenz möglich bleibt. private static void ScheduledReceivePlanCarriesAdapterMetadata() { var snapshot = Snapshot("APP", "SP", ArtifactStates.SendPortStarted); snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RV_PMP_Trigger_Schedule", Enabled = true, AdapterName = "Schedule", Address = "scheduler://PMP-trigger" }); var service = new BizTalkOperationService(null); var shutdown = service.CreateShutdownPlan(snapshot, snapshot.Server); var step = shutdown.Steps.Single(x => x.Kind == "ReceiveLocation"); Assert(step.AdapterName == "Schedule", "adapter name missing from shutdown plan"); Assert(step.Address == "scheduler://PMP-trigger", "scheduler URI missing from shutdown plan"); InTemp(directory => { var path = Path.Combine(directory, "plan.json"); JsonFileStore.Save(path, shutdown); var loaded = JsonFileStore.Load(path); Assert(loaded.Steps.Single(x => x.Kind == "ReceiveLocation").Address.StartsWith("scheduler:", StringComparison.OrdinalIgnoreCase), "scheduler metadata did not survive serialization"); }); } /// Prüft Dateinamen- und vollständige Assemblyidentität vor prozesslokalem Laden. private static void AdapterAssemblyResolverRequiresMatchingIdentity() { InTemp(directory => { var source = typeof(BizTalkOperationService).Assembly.Location; var identity = AssemblyName.GetAssemblyName(source); var candidate = Path.Combine(directory, identity.Name + ".dll"); File.Copy(source, candidate); Assert(AdapterAssemblyResolver.FindCandidateFile(identity.Name, identity, new[] { directory }) == candidate, "matching assembly identity was not found"); var wrongVersion = new AssemblyName(identity.FullName); wrongVersion.Version = new Version(identity.Version.Major + 1, 0, 0, 0); Assert(AdapterAssemblyResolver.FindCandidateFile(identity.Name, wrongVersion, new[] { directory }) == null, "wrong assembly version was accepted"); File.Copy(source, Path.Combine(directory, AdapterAssemblyResolver.SchedulerAssemblyName + ".dll")); Assert(AdapterAssemblyResolver.FindCandidateFile(AdapterAssemblyResolver.SchedulerAssemblyName, null, new[] { directory }) == null, "file-name-only scheduler impostor was accepted"); }); } /// Prüft Neustart-Rehydration, Vortagskompression und die 30-Tage-Grenze. private static void OperationLogPersistsCompressesAndRetainsThirtyDays() { InTemp(directory => { var today = DateTime.Today; var yesterday = Path.Combine(directory, "BizTalkPlatformManagementTool-" + today.AddDays(-1).ToString("yyyy-MM-dd") + ".log"); var expired = Path.Combine(directory, "BizTalkPlatformManagementTool-" + today.AddDays(-30).ToString("yyyy-MM-dd") + ".log"); File.WriteAllText(yesterday, "[" + today.AddDays(-1).ToString("yyyy-MM-dd") + " 08:00:00][WARNING] retained warning\r\n", Encoding.UTF8); File.WriteAllText(expired, "[" + today.AddDays(-30).ToString("yyyy-MM-dd") + " 08:00:00][INFO] expired\r\n", Encoding.UTF8); var logger = new OperationLogger(null, directory); Assert(File.Exists(yesterday + ".gz") && !File.Exists(yesterday), "completed daily log was not compressed"); Assert(!File.Exists(expired), "log outside 30-day retention was not deleted"); logger.Error("durable failure marker"); var restarted = new OperationLogger(null, directory); var entries = restarted.ReadRecentEntries(100); Assert(entries.Any(x => x.Level == LogLevel.Warning && x.Message == "retained warning"), "compressed history was not restored"); Assert(entries.Any(x => x.Level == LogLevel.Error && x.Message == "durable failure marker"), "current history was not restored after restart"); }); } /// Prüft, dass erst ein real beschreibbarer Pfad als aktiv gilt. private static void OperationLogUsesVerifiedStartupFallback() { InTemp(directory => { var blocked = Path.Combine(directory, "blocked-primary"); var fallback = Path.Combine(directory, "writable-fallback"); File.WriteAllText(blocked, "not a directory"); var logger = new OperationLogger(null, new[] { blocked, fallback }); Assert(logger.IsFileLoggingAvailable, "writable fallback was not activated"); Assert(string.Equals(logger.LogDirectory, fallback, StringComparison.OrdinalIgnoreCase), "wrong fallback directory selected"); Assert(logger.StorageWarning.Contains("Primary runtime log path is not writable"), "startup fallback warning missing"); logger.Info("verified fallback marker"); Assert(File.Exists(logger.LogFilePath), "fallback log file was not created"); Assert(File.ReadAllText(logger.LogFilePath).Contains("verified fallback marker"), "fallback append was not durable"); }); } /// Prüft automatisches Failover, wenn der aktive Pfad später ausfällt. private static void OperationLogFailsOverAfterAppendFailure() { InTemp(directory => { var primary = Path.Combine(directory, "primary"); var fallback = Path.Combine(directory, "fallback"); var visible = new List(); var logger = new OperationLogger(visible.Add, new[] { primary, fallback }); Assert(string.Equals(logger.LogDirectory, primary, StringComparison.OrdinalIgnoreCase), "primary directory was not initially selected"); Directory.Delete(primary, true); File.WriteAllText(primary, "now blocked"); logger.Error("runtime failover marker"); Assert(string.Equals(logger.LogDirectory, fallback, StringComparison.OrdinalIgnoreCase), "append failure did not switch to fallback"); Assert(File.ReadAllText(logger.LogFilePath).Contains("runtime failover marker"), "failed primary record was not written to fallback"); Assert(visible.Any(x => x.Level == LogLevel.Warning && x.Message.Contains("Runtime log path switched")), "runtime fallback was not visible in the sink"); }); } /// Prüft eine sichtbare Warnung, wenn kein Dateipfad beschreibbar ist. private static void OperationLogSurfacesTotalStorageFailure() { InTemp(directory => { var blockedOne = Path.Combine(directory, "blocked-one"); var blockedTwo = Path.Combine(directory, "blocked-two"); File.WriteAllText(blockedOne, "not a directory"); File.WriteAllText(blockedTwo, "not a directory"); var visible = new List(); var logger = new OperationLogger(visible.Add, new[] { blockedOne, blockedTwo }); Assert(!logger.IsFileLoggingAvailable, "logger reported unavailable candidates as writable"); Assert(logger.StorageWarning.Contains("RUNTIME FILE LOGGING UNAVAILABLE"), "total startup failure diagnostic missing"); logger.Error("grid-only marker"); Assert(visible.Any(x => x.Message == "grid-only marker"), "business entry disappeared with file logging failure"); Assert(visible.Any(x => x.Level == LogLevel.Warning && x.Message.Contains("RUNTIME FILE LOGGING UNAVAILABLE")), "file logging failure was not visible in the grid sink"); }); } /// Prüft Typ, HRESULT, innere Ausnahme und Stacktrace für Supportfälle. private static void ExceptionDiagnosticsContainSupportContext() { Exception failure; try { throw new InvalidOperationException("outer detail", new FileNotFoundException("scheduler dependency missing")); } catch (Exception ex) { failure = ex; } var diagnostic = ExceptionDiagnostics.Format(failure); Assert(diagnostic.Contains("Type=System.InvalidOperationException"), "exception type missing"); Assert(diagnostic.Contains("HResult=0x"), "HRESULT missing"); Assert(diagnostic.Contains("InnerException[1]"), "inner exception index missing"); Assert(diagnostic.Contains("scheduler dependency missing"), "inner message missing"); Assert(diagnostic.Contains("StackTrace="), "stack trace missing"); } /// Prüft den vollständigen Shutdown-Fortgang von Receive Location bis Host Instance nach einem frühen Fehler. private static void ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure() { var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted); snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RV_PMP_Trigger_Schedule", Enabled = true }); snapshot.Applications[0].Orchestrations.Add(new OrchestrationState { Application = "APP", Name = "ORCHESTRATION", OrchestrationStatus = ArtifactStates.OrchestrationStarted }); snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HOST:SERVER", HostName = "HOST", Server = snapshot.Server, RawState = ArtifactStates.HostStarted }); var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server); var runtime = new FakeOperationStepRuntime { FailingName = "RV_PMP_Trigger_Schedule" }; var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime); Assert(runtime.Calls.SequenceEqual(new[] { "RV_PMP_Trigger_Schedule", "ORCHESTRATION", "SEND", "HOST:SERVER" }), "shutdown did not continue through every later artifact category"); Assert(report.FailedCount == 1 && report.SucceededCount == 3, "cross-category shutdown outcome is incomplete"); } /// Prüft ENTSSO als erste Voraussetzung des Emergency Restore. private static void EmergencyRestorePlanStartsSsoFirst() { var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted); snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HOST:SERVER", HostName = "HOST", Server = snapshot.Server, RawState = ArtifactStates.HostStarted }); var plan = new BizTalkOperationService(null).CreateEmergencyRestorePlan(snapshot, snapshot.Server); Assert(plan.Mode == OperationMode.EmergencyRestore.ToString(), "emergency restore mode missing"); Assert(plan.Steps.First().Kind == "WindowsService" && plan.Steps.First().Name == "ENTSSO", "ENTSSO must be the first emergency step"); Assert(plan.Steps[1].Kind == "HostInstance", "host instance must follow ENTSSO"); } /// Prüft, dass Kurzname und FQDN dieselbe Host Instance nicht fälschlich überspringen. private static void HostInstancePlanAcceptsShortAndFqdnServer() { var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted); snapshot.Server = "BIZTALK-A.example.local"; snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HOST:BIZTALK-A", HostName = "HOST", Server = "BIZTALK-A.example.local", RawState = ArtifactStates.HostStarted }); var plan = new BizTalkOperationService(null).CreateRestorePlan(snapshot, "BIZTALK-A"); Assert(plan.Steps.First(x => x.Kind == "HostInstance").Execute, "short name/FQDN match incorrectly skipped host instance"); } /// Prüft, dass eine Scheduler-artige Exception spätere Schritte nicht mehr verhindert. private static void PlanExecutionContinuesAfterSchedulerFailure() { var plan = TestPlan("RL-A", "RV_PMP_Trigger_Schedule", "RL-C"); var runtime = new FakeOperationStepRuntime { FailingName = "RV_PMP_Trigger_Schedule" }; var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime); Assert(runtime.Calls.SequenceEqual(new[] { "RL-A", "RV_PMP_Trigger_Schedule", "RL-C" }), "executor stopped after isolated failure"); Assert(report.SucceededCount == 2 && report.FailedCount == 1, "unexpected continuation summary"); Assert(report.Steps[1].Outcome == OperationStepOutcomes.Failed, "failed step outcome missing"); Assert(report.Steps[1].Error.Contains("Microsoft.BizTalk.Scheduler"), "nested Scheduler exception missing from report"); } /// Prüft die Fortsetzung auch bei mehreren unterschiedlichen, nicht adapterspezifischen Exceptions. private static void PlanExecutionContinuesAfterMultipleUnexpectedExceptions() { var runtime = new MultipleFailureOperationStepRuntime(); var report = new OperationPlanExecutor(null).Execute(TestPlan("INVALID", "IO", "TIMEOUT", "CONTINUE"), TestOptions(false), runtime); Assert(runtime.Calls.SequenceEqual(new[] { "INVALID", "IO", "TIMEOUT", "CONTINUE" }), "an unexpected exception stopped later independent steps"); Assert(report.FailedCount == 3 && report.SucceededCount == 1, "multiple failure summary is incomplete"); Assert(report.Steps.All(x => !string.IsNullOrWhiteSpace(x.FinishedAt)), "a failure result has no completion timestamp"); } /// Prüft, dass selbst eine defekte GUI-Logweiterleitung keine fachlichen Schritte verhindert. private static void LoggerSinkFailureCannotAbortPlanExecution() { var logger = new OperationLogger(entry => { throw new InvalidOperationException("simulated UI sink failure"); }); var runtime = new FakeOperationStepRuntime { FailingName = "TWO" }; var report = new OperationPlanExecutor(logger).Execute(TestPlan("ONE", "TWO", "THREE"), TestOptions(false), runtime); Assert(runtime.Calls.SequenceEqual(new[] { "ONE", "TWO", "THREE" }), "logging failure stopped plan execution"); Assert(report.SucceededCount == 2 && report.FailedCount == 1, "logging failure changed business outcomes"); Assert(report.Steps[1].Error.Contains("Microsoft.BizTalk.Scheduler"), "business failure was lost while the log sink was failing"); } /// Prüft idempotentes Überspringen eines bereits erreichten Sollzustands. private static void PlanExecutionSkipsAlreadySatisfiedState() { var plan = TestPlan("ALREADY", "CHANGE"); var runtime = new FakeOperationStepRuntime(); runtime.AlreadySatisfiedNames.Add("ALREADY"); var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime); Assert(report.AlreadySatisfiedCount == 1 && report.SucceededCount == 1 && report.FailedCount == 0, "idempotent outcome counts are wrong"); Assert(runtime.MutatedNames.SequenceEqual(new[] { "CHANGE" }), "already-satisfied step was mutated"); } /// Prüft die dauerhafte Serialisierung eines Teilfehler-Reports. private static void ExecutionReportRoundTripPreservesFailure() { InTemp(directory => { var report = new OperationPlanExecutor(null).Execute(TestPlan("FAIL", "CONTINUE"), TestOptions(false), new FakeOperationStepRuntime { FailingName = "FAIL" }); var path = Path.Combine(directory, "restore-result.json"); JsonFileStore.Save(path, report); var loaded = JsonFileStore.Load(path); Assert(loaded.HasFailures && loaded.FailedCount == 1, "serialized report lost failure summary"); Assert(loaded.Steps.Count == 2 && loaded.Steps[1].Outcome == OperationStepOutcomes.Succeeded, "serialized report lost continued step"); }); } /// Prüft die Neutralisierung formelartiger CSV-Feldwerte. private static void CsvNeutralizesFormulaValues() { InTemp(directory => { var path = Path.Combine(directory, "diff.csv"); var diff = new SnapshotDiff(); diff.ArtifactDifferences.Add(new ArtifactDiffEntry { Application = "=cmd|' /C calc'!A0", ArtifactType = "SendPort", Name = "PORT", Before = "Started", After = "Stopped" }); CsvWriter.WriteDiff(path, diff); Assert(File.ReadAllText(path).Contains("'=cmd"), "formula-like CSV field was not neutralized"); }); } /// Prüft, dass eine nach Manifestbildung veränderte Payload abgelehnt wird. private static void PackageManifestRejectsTampering() { InTemp(directory => { var app = Path.Combine(directory, "application"); Directory.CreateDirectory(app); File.WriteAllText(Path.Combine(app, InstallerEngine.ApplicationExeName), "payload"); File.WriteAllText(Path.Combine(app, InstallerEngine.ApplicationExeName + ".config"), "config"); var manifest = Path.Combine(directory, "application.manifest"); PackageManifest.Write(app, manifest); PackageManifest.ValidateAndRead(app, manifest); File.AppendAllText(Path.Combine(app, InstallerEngine.ApplicationExeName), "tampered"); Expect(() => PackageManifest.ValidateAndRead(app, manifest)); }); } /// Prüft die Aktivierung einer vollständig validierten Neuinstallation. private static void InstallerActivatesValidatedPayload() { InTemp(directory => { var package = CreatePackage(directory, "new"); var install = Path.Combine(directory, "install"); var data = Path.Combine(directory, "data"); var engine = new InstallerEngine(package, install, data, false, path => File.Exists(path)); engine.Install(false, null); Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "new", "new payload not activated"); Assert(File.Exists(Path.Combine(install, "install-state.txt")), "install state missing"); }); } /// Prüft die erfolgreiche Aktivierung nach einer kurzzeitigen Rename-Sperre. private static void InstallerRetriesTransientActivationMove() { InTemp(directory => { var package = CreatePackage(directory, "new"); var install = Path.Combine(directory, "install"); var data = Path.Combine(directory, "data"); var moveCalls = 0; var delays = new List(); var engine = new InstallerEngine( package, install, data, false, path => true, (source, target) => { moveCalls++; if (moveCalls == 1) throw new IOException("simulated transient scanner lock"); Directory.Move(source, target); }, delays.Add); engine.Install(false, null); Assert(moveCalls == 2, "transient activation move was not retried exactly once"); Assert(delays.SequenceEqual(new[] { 250 }), "unexpected retry delay for transient activation move"); Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "new", "payload was not activated after retry"); var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single()); Assert(log.Contains("event=directory_move_retry role=activate failed_attempt=1"), "transient move retry was not diagnosed"); Assert(log.Contains("event=directory_move_recovered role=activate attempt=2"), "move recovery was not diagnosed"); }); } /// Prüft den verifizierten Kopierfallback einer durchgehend gesperrten Neuinstallation. private static void InstallerUsesVerifiedCopyFallbackForNewInstall() { InTemp(directory => { var package = CreatePackage(directory, "new"); var install = Path.Combine(directory, "install"); var data = Path.Combine(directory, "data"); var moveCalls = 0; var delays = new List(); var engine = new InstallerEngine( package, install, data, false, path => true, (source, target) => { moveCalls++; throw new UnauthorizedAccessException("simulated permanent policy denial"); }, delays.Add); engine.Install(false, null); Assert(moveCalls == 8, "permanent move failure did not stop after eight attempts"); Assert(delays.SequenceEqual(new[] { 250, 500, 1000, 2000, 3000, 5000, 8000 }), "bounded retry schedule changed unexpectedly"); Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "new", "verified copy fallback did not activate payload"); Assert(!Directory.GetDirectories(directory, "install.staging.*").Any(), "staging remained after copy fallback"); var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single()); Assert(log.Contains("event=directory_move_retry role=activate failed_attempt=7"), "final scheduled retry was not diagnosed"); Assert(log.Contains("exception_type=System.UnauthorizedAccessException"), "ACL failure type missing from retry diagnostics"); Assert(log.Contains("event=activation_fallback_complete method=verified_copy"), "copy fallback completion was not diagnosed"); Assert(log.Contains("scope=new_install"), "new-install fallback scope missing from diagnostics"); Assert(log.Contains("activation_method=verified_copy_fallback"), "copy fallback missing from setup summary"); }); } /// Prüft den verifizierten Kopierfallback, nachdem eine Update-Vorversion atomar gesichert wurde. private static void InstallerUsesVerifiedCopyFallbackForBackedUpUpdate() { InTemp(directory => { var package = CreatePackage(directory, "new"); var install = Path.Combine(directory, "install"); Directory.CreateDirectory(install); File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "old"); File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config"); var data = Path.Combine(directory, "data"); var activationMoveAttempts = 0; var delays = new List(); var engine = new InstallerEngine( package, install, data, false, path => true, (source, target) => { if (source.IndexOf(".staging.", StringComparison.Ordinal) >= 0) { activationMoveAttempts++; throw new UnauthorizedAccessException("simulated staging rename policy denial"); } Directory.Move(source, target); }, delays.Add); engine.Install(false, null); Assert(activationMoveAttempts == 8, "update activation did not exhaust the bounded move attempts"); Assert(delays.SequenceEqual(new[] { 250, 500, 1000, 2000, 3000, 5000, 8000 }), "unexpected update fallback retry schedule"); Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "new", "update copy fallback did not activate the new payload"); Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "successful update fallback left a backup directory"); var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single()); Assert(log.Contains("event=activation_fallback_complete method=verified_copy scope=update_after_backup"), "update fallback scope missing from diagnostics"); Assert(log.Contains("activation_method=verified_copy_fallback"), "update copy fallback missing from setup summary"); }); } /// Prüft das Backup-Rollback, wenn der Ziel-Self-Test nach einem Update-Kopierfallback fehlschlägt. private static void InstallerRollsBackFailedUpdateCopyFallback() { InTemp(directory => { var package = CreatePackage(directory, "new"); var install = Path.Combine(directory, "install"); Directory.CreateDirectory(install); File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "old"); File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config"); var selfTestCalls = 0; var engine = new InstallerEngine( package, install, Path.Combine(directory, "data"), false, path => ++selfTestCalls == 1, (source, target) => { if (source.IndexOf(".staging.", StringComparison.Ordinal) >= 0) throw new UnauthorizedAccessException("simulated staging rename policy denial"); Directory.Move(source, target); }, milliseconds => { }); var exception = Capture(() => engine.Install(false, null)); Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "failed update fallback did not restore the previous payload"); Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "rollback left the previous version in a backup directory"); Assert(!Directory.GetDirectories(directory, "install.staging.*").Any(), "rollback left the failed staging directory"); Assert(exception.Message.Contains("Fehlercode=SETUP-ACTIVATED-SELFTEST"), "failed fallback reported the wrong setup phase"); Assert(exception.Message.Contains("Rollback erfolgreich"), "failed fallback did not report successful rollback"); }); } /// Prüft, dass ein Update bei dauerhaft gesperrtem Backup atomar und unverändert abbricht. private static void InstallerStopsAfterBoundedUpdateMoveRetries() { InTemp(directory => { var package = CreatePackage(directory, "new"); var install = Path.Combine(directory, "install"); Directory.CreateDirectory(install); File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "old"); File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config"); var data = Path.Combine(directory, "data"); var moveCalls = 0; var delays = new List(); var engine = new InstallerEngine( package, install, data, false, path => true, (source, target) => { moveCalls++; throw new UnauthorizedAccessException("simulated permanent update policy denial"); }, delays.Add); var exception = Capture(() => engine.Install(false, null)); Assert(moveCalls == 8, "permanent update move failure did not stop after eight attempts"); Assert(delays.SequenceEqual(new[] { 250, 500, 1000, 2000, 3000, 5000, 8000 }), "bounded update retry schedule changed unexpectedly"); Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "failed update modified the active installation"); Assert(!Directory.GetDirectories(directory, "install.staging.*").Any(), "staging remained after permanent update failure"); Assert(exception.Message.Contains("Fehlercode=SETUP-ACTIVATION"), "activation error code missing after update retries"); Assert(exception.Message.Contains("Kein Rollback erforderlich"), "pre-mutation update denial reported a rollback"); var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single()); Assert(log.Contains("event=directory_move_retry role=backup failed_attempt=7"), "final update retry was not diagnosed"); Assert(!log.Contains("event=activation_fallback_started"), "update incorrectly used the new-install copy fallback"); }); } /// Prüft die Ablehnung nicht deklarierter Dateien und ausbrechender Manifestpfade. private static void PackageManifestRejectsUndeclaredAndTraversalFiles() { InTemp(directory => { var app = Path.Combine(directory, "application"); Directory.CreateDirectory(app); var exe = Path.Combine(app, InstallerEngine.ApplicationExeName); File.WriteAllText(exe, "payload"); File.WriteAllText(Path.Combine(app, InstallerEngine.ApplicationExeName + ".config"), "config"); var manifest = Path.Combine(directory, "application.manifest"); PackageManifest.Write(app, manifest); File.WriteAllText(Path.Combine(app, "undeclared.dll"), "extra"); Expect(() => PackageManifest.ValidateAndRead(app, manifest)); File.Delete(Path.Combine(app, "undeclared.dll")); File.WriteAllText(manifest, PackageManifest.Sha256(exe) + "|" + new FileInfo(exe).Length + "|../escape.exe" + Environment.NewLine); Expect(() => PackageManifest.ValidateAndRead(app, manifest)); }); } /// Prüft, dass ein Staging-Fehler die aktive Installation nicht mutiert. private static void InstallerDoesNotMutateOnStagingFailure() { InTemp(directory => { var package = CreatePackage(directory, "new"); var install = Path.Combine(directory, "install"); Directory.CreateDirectory(install); File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "old"); File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config"); var data = Path.Combine(directory, "data"); var messages = new List(); var engine = new InstallerEngine(package, install, data, false, path => false); var exception = Capture(() => engine.Install(false, messages.Add)); Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "staging failure modified the installed payload"); Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "backup was created before staging passed"); Assert(exception.Message.Contains("Fehlercode=SETUP-STAGING-VALIDATION"), "staging error code missing"); Assert(exception.Message.Contains("Kein Rollback erforderlich"), "pre-mutation failure reported a rollback"); Assert(messages.Any(x => x.Contains("Kein Rollback erforderlich")), "operator output did not distinguish pre-mutation failure"); var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single()); Assert(log.Contains("event=self_test_completed label=staging"), "staging self-test result missing from log"); Assert(log.Contains("event=rollback_summary result=not_required"), "no-rollback evidence missing from log"); }); } /// Prüft die Wiederherstellung der Vorversion nach fehlgeschlagenem aktiviertem Self-Test. private static void InstallerRollsBackFailedActivatedSelfTest() { InTemp(directory => { var package = CreatePackage(directory, "new"); var install = Path.Combine(directory, "install"); Directory.CreateDirectory(install); File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "old"); File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config"); var calls = 0; var engine = new InstallerEngine(package, install, Path.Combine(directory, "data"), false, path => ++calls == 1); var exception = Capture(() => engine.Install(false, null)); Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "previous payload was not restored"); Assert(!Directory.GetDirectories(directory, "install.staging.*").Any(), "staging directory remained"); Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "backup directory remained"); Assert(exception.Message.Contains("Fehlercode=SETUP-ACTIVATED-SELFTEST"), "activated self-test error code missing"); Assert(exception.Message.Contains("Rollback erfolgreich"), "successful rollback not reported"); }); } /// Prüft die Deinstallation über ein atomar umbenanntes Quarantäneverzeichnis. private static void InstallerUninstallRemovesProgramDirectory() { InTemp(directory => { var package = CreatePackage(directory, "new"); var install = Path.Combine(directory, "install"); Directory.CreateDirectory(install); File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "installed"); var engine = new InstallerEngine(package, install, Path.Combine(directory, "data"), false, path => true); engine.Uninstall(null); Assert(!Directory.Exists(install), "program directory still exists after uninstall"); Assert(!Directory.GetDirectories(directory, "install.removed.*").Any(), "uninstall quarantine directory remained"); }); } /// Prüft technischen Kontext, Fehlercode, HRESULT und innere Ausnahme im Setup-Log. private static void InstallerDiagnosticLogContainsContextAndExceptionChain() { InTemp(directory => { var log = SetupOperationLog.Create(directory, "diagnostic-test"); Assert(!string.IsNullOrEmpty(log.FilePath), "diagnostic log was not created"); Exception failure; try { throw new InvalidOperationException("outer", new IOException("inner")); } catch (Exception ex) { failure = ex; } log.WriteException("TEST-CODE", "Test phase", failure); var content = File.ReadAllText(log.FilePath); Assert(content.Contains("event=setup_started"), "setup context header missing"); Assert(content.Contains("process_bitness="), "process bitness missing"); Assert(content.Contains("identity="), "identity missing"); Assert(content.Contains("error_code=TEST-CODE"), "error code missing"); Assert(content.Contains("inner[1]"), "inner exception missing"); Assert(content.Contains("hresult=0x"), "HRESULT missing"); }); } /// Prüft, dass ein fehlerhafter UI-Callback die Installation nicht beeinflusst. private static void InstallerSurvivesUiReportFailure() { InTemp(directory => { var package = CreatePackage(directory, "new"); var install = Path.Combine(directory, "install"); var data = Path.Combine(directory, "data"); var engine = new InstallerEngine(package, install, data, false, path => true); engine.Install(false, message => { throw new InvalidOperationException("UI unavailable"); }); Assert(File.Exists(Path.Combine(install, InstallerEngine.ApplicationExeName)), "UI report failure interrupted installation"); var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single()); Assert(log.Contains("event=ui_report_failed"), "UI report failure was not diagnosed"); }); } /// Prüft, dass optionale Desktop- und Warnungsfehler niemals eine Kernoperation abbrechen. private static void OptionalWindowsIntegrationFailuresAreNonFatal() { InTemp(directory => { var log = SetupOperationLog.Create(directory, "optional-integration-test"); var warnings = new List(); var failed = InstallerEngine.TryOptionalWindowsIntegrationStep( "desktop_shortcut_create", "Desktop-Verknuepfung erstellen", Path.Combine(directory, "Public Desktop", "Tool.lnk"), () => { throw new UnauthorizedAccessException("simulated public desktop ACL denial"); }, log, warnings.Add); Assert(!failed, "optional ACL failure was reported as success"); Assert(warnings.Count == 1 && warnings[0].Contains("Kernoperation wird fortgesetzt"), "operator continuation warning missing"); var warningFailureEscaped = false; try { InstallerEngine.TryOptionalWindowsIntegrationStep( "desktop_shortcut_validate", "Desktop-Verknuepfung validieren", Path.Combine(directory, "Public Desktop", "Tool.lnk"), () => { throw new InvalidOperationException("simulated state mismatch"); }, log, message => { throw new InvalidOperationException("simulated warning sink failure"); }); } catch { warningFailureEscaped = true; } Assert(!warningFailureEscaped, "optional warning sink failure escaped the isolation boundary"); var succeeded = InstallerEngine.TryOptionalWindowsIntegrationStep( "desktop_shortcut_remove", "Desktop-Verknuepfung entfernen", Path.Combine(directory, "Public Desktop", "Tool.lnk"), () => { }, log, warnings.Add); Assert(succeeded, "successful optional operation was reported as failure"); var content = File.ReadAllText(log.FilePath); Assert(content.Contains("event=optional_windows_integration_warning role=desktop_shortcut_create"), "optional ACL failure missing from diagnostics"); Assert(content.Contains("event=optional_windows_integration_report_failed role=desktop_shortcut_validate"), "warning sink failure missing from diagnostics"); Assert(content.Contains("event=optional_windows_integration_completed role=desktop_shortcut_remove"), "optional success missing from diagnostics"); }); } /// Prüft den Diagnose-Log-Fallback bei einem nicht verwendbaren ProgramData-Pfad. private static void InstallerDiagnosticLogFallsBackToTemp() { InTemp(directory => { var blockedDataDirectory = Path.Combine(directory, "blocked-data-directory"); File.WriteAllText(blockedDataDirectory, "not a directory"); var log = SetupOperationLog.Create(blockedDataDirectory, "fallback-test"); try { Assert(log.IsFallback, "diagnostic log did not use the Temp fallback"); Assert(!string.IsNullOrEmpty(log.CreationError), "primary logging failure was not retained"); Assert(File.Exists(log.FilePath), "fallback diagnostic log was not created"); Assert(File.ReadAllText(log.FilePath).Contains("event=primary_log_creation_failed"), "fallback reason missing from log"); } finally { if (File.Exists(log.FilePath)) File.Delete(log.FilePath); } }); } /// Erstellt einen minimalen ausführbaren Testplan. /// Die Namen der Testschritte in Ausführungsreihenfolge. /// Ein Restore-Testplan. private static OperationPlan TestPlan(params string[] names) { var plan = new OperationPlan { Mode = OperationMode.Restore.ToString(), CreatedAt = DateTimeOffset.Now.ToString("o"), Server = Environment.MachineName }; foreach (var name in names) { plan.Steps.Add(new OperationStep { Kind = "ReceiveLocation", Application = "APP", Name = name, Action = "Restore " + name, WmiClass = "MSBTS_ReceiveLocation", KeyProperty = "Name", KeyValue = name, MethodName = "Enable", Execute = true }); } return plan; } /// Erstellt konsistente Optionen für portable Executor-Tests. /// True für einen simulierenden Lauf. /// Testoptionen. private static OperationOptions TestOptions(bool dryRun) { return new OperationOptions { Server = Environment.MachineName, OutputDirectory = Path.GetTempPath(), StateFile = "before.json", DryRun = dryRun, WaitTimeoutSeconds = 30, PollIntervalSeconds = 1 }; } /// Simuliert bereits erreichte Zustände, Mutationen und isolierte Laufzeitfehler. private sealed class FakeOperationStepRuntime : IOperationStepRuntime { /// Initialisiert die Aufruf- und Mutationslisten. public FakeOperationStepRuntime() { Calls = new List(); MutatedNames = new List(); AlreadySatisfiedNames = new HashSet(StringComparer.OrdinalIgnoreCase); } /// Gets or sets the step name that throws the simulated Scheduler error. public string FailingName { get; set; } /// Gets every runtime call in order. public List Calls { get; private set; } /// Gets only steps that required a simulated mutation. public List MutatedNames { get; private set; } /// Gets the names reported as already satisfied. public HashSet AlreadySatisfiedNames { get; private set; } /// Simuliert einen zustandsbewussten Laufzeitschritt. /// Der Testschritt. /// Die ungenutzten Testoptionen. /// Das simulierte Ergebnis. public RuntimeStepOutcome Execute(OperationStep step, OperationOptions options) { Calls.Add(step.Name); if (string.Equals(step.Name, FailingName, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException( "Could not validate TransportTypeData.", new FileNotFoundException("Could not load file or assembly Microsoft.BizTalk.Scheduler, Version=3.13.0.0.")); } if (AlreadySatisfiedNames.Contains(step.Name)) { return RuntimeStepOutcome.AlreadySatisfied; } MutatedNames.Add(step.Name); return RuntimeStepOutcome.Succeeded; } } /// Wirft verschiedene Exception-Typen und liefert danach wieder ein erfolgreiches Ergebnis. private sealed class MultipleFailureOperationStepRuntime : IOperationStepRuntime { /// Initialisiert die vollständige Aufrufliste. public MultipleFailureOperationStepRuntime() { Calls = new List(); } /// Gets all attempted step names. public List Calls { get; private set; } /// Wirft je nach Testschritt eine andere unerwartete Ausnahme. /// Der auszuführende Testschritt. /// Die ungenutzten Testoptionen. /// Erfolg für den abschließenden Fortsetzungsschritt. public RuntimeStepOutcome Execute(OperationStep step, OperationOptions options) { Calls.Add(step.Name); if (step.Name == "INVALID") throw new InvalidOperationException("unexpected state error"); if (step.Name == "IO") throw new IOException("unexpected provider I/O error"); if (step.Name == "TIMEOUT") throw new TimeoutException("unexpected provider timeout"); return RuntimeStepOutcome.Succeeded; } } /// Erstellt einen minimalen Snapshot für Vergleiche und Planprüfungen. /// Der Name der Testanwendung. /// Der Name des Test-Send-Ports. /// Der rohe Send-Port-Status. /// Ein Snapshot mit einer Anwendung und einem Send Port. private static BizTalkSnapshot Snapshot(string application, string port, int state) { var result = new BizTalkSnapshot { ToolVersion = "test", CreatedAt = DateTimeOffset.Now.ToString("o"), Server = Environment.MachineName }; var app = new ApplicationSnapshot { Application = application }; app.SendPorts.Add(new SendPortState { Application = application, Name = port, Status = state }); result.Applications.Add(app); return result; } /// Erzeugt eine minimale, manifestierte Installer-Payload. /// Das temporäre Testwurzelverzeichnis. /// Der simulierte Inhalt der Anwendungs-EXE. /// Das Verzeichnis des erzeugten Testpakets. private static string CreatePackage(string root, string payload) { var package = Path.Combine(root, "package"); var app = Path.Combine(package, "application"); Directory.CreateDirectory(app); File.WriteAllText(Path.Combine(app, InstallerEngine.ApplicationExeName), payload); File.WriteAllText(Path.Combine(app, InstallerEngine.ApplicationExeName + ".config"), "config"); PackageManifest.Write(app, Path.Combine(package, "application.manifest")); return package; } /// Führt einen Test in einem eindeutigen temporären Verzeichnis mit garantierter Bereinigung aus. /// Die Testfunktion, die den temporären Pfad erhält. private static void InTemp(Action action) { var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.Tests." + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(directory); try { action(directory); } finally { if (Directory.Exists(directory)) Directory.Delete(directory, true); } } /// Bricht den Test ab, wenn eine erwartete Bedingung nicht erfüllt ist. /// Die erwartete Bedingung. /// Die Fehlermeldung bei nicht erfüllter Bedingung. private static void Assert(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); } /// Prüft, dass eine Aktion eine bestimmte Ausnahme auslöst. /// Der erwartete Ausnahmetyp. /// Die auszuführende Aktion. private static void Expect(Action action) where T : Exception { try { action(); } catch (T) { return; } throw new InvalidOperationException("Expected exception " + typeof(T).Name); } /// Führt eine Aktion aus und gibt die erwartete Ausnahme für weitere Prüfungen zurück. /// Der erwartete Ausnahmetyp. /// Die auszuführende Aktion. /// Die von der Aktion ausgelöste Ausnahme. private static T Capture(Action action) where T : Exception { try { action(); } catch (T ex) { return ex; } throw new InvalidOperationException("Expected exception " + typeof(T).Name); } } }