1154 lines
74 KiB
C#
1154 lines
74 KiB
C#
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;
|
|
using BizTalkPlatformManagementTool.Ui;
|
|
|
|
namespace BizTalkPlatformManagementTool.Tests
|
|
{
|
|
/// <summary>
|
|
/// Enthält die portable Regressionstestsuite ohne Abhängigkeit von einem externen Testframework.
|
|
/// </summary>
|
|
internal static class Program
|
|
{
|
|
/// <summary>Anzahl der im aktuellen Testlauf fehlgeschlagenen Prüfungen.</summary>
|
|
private static int failures;
|
|
|
|
/// <summary>Führt alle Regressionstests aus und liefert einen CI-tauglichen Exitcode.</summary>
|
|
/// <returns>Null, wenn alle Tests bestanden wurden; andernfalls eins.</returns>
|
|
private static int Main()
|
|
{
|
|
Run("JsonRoundTripIsBomTolerantAndAtomic", JsonRoundTripIsBomTolerantAndAtomic);
|
|
Run("DiffUsesApplicationAndNameIdentity", DiffUsesApplicationAndNameIdentity);
|
|
Run("RestoreRejectsDifferentServer", RestoreRejectsDifferentServer);
|
|
Run("Legacy213SnapshotIsAcceptedForRecovery", Legacy213SnapshotIsAcceptedForRecovery);
|
|
Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder);
|
|
Run("ShutdownPlanUsesGlobalSafeOrder", ShutdownPlanUsesGlobalSafeOrder);
|
|
Run("ShutdownPlanRequiresCheckpointBeforeLaterWorkWithoutReceiveLocations", ShutdownPlanRequiresCheckpointBeforeLaterWorkWithoutReceiveLocations);
|
|
Run("ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure", ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure);
|
|
Run("ShutdownCheckpointDeclineStopsLaterPhases", ShutdownCheckpointDeclineStopsLaterPhases);
|
|
Run("ShutdownCheckpointMissingHandlerFailsClosed", ShutdownCheckpointMissingHandlerFailsClosed);
|
|
Run("ShutdownCheckpointDryRunDoesNotPrompt", ShutdownCheckpointDryRunDoesNotPrompt);
|
|
Run("ShutdownOperatorPromptsDistinguishDryRunAndRealDecision", ShutdownOperatorPromptsDistinguishDryRunAndRealDecision);
|
|
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("ApplicationResolverMapsArtifactsAndRejectsAmbiguity", ApplicationResolverMapsArtifactsAndRejectsAmbiguity);
|
|
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;
|
|
}
|
|
|
|
/// <summary>Führt einen einzelnen Test isoliert aus und protokolliert sein Ergebnis.</summary>
|
|
/// <param name="name">Der stabile Testname für die Konsolenausgabe.</param>
|
|
/// <param name="test">Die auszuführende Testfunktion.</param>
|
|
private static void Run(string name, Action test)
|
|
{
|
|
try { test(); Console.WriteLine("PASS " + name); }
|
|
catch (Exception ex) { failures++; Console.Error.WriteLine("FAIL " + name + ": " + ex); }
|
|
}
|
|
|
|
/// <summary>Prüft BOM-tolerantes Lesen und rückstandsfreies atomisches JSON-Schreiben.</summary>
|
|
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<BizTalkSnapshot>(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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft, dass gleichnamige Artefakte verschiedener Anwendungen getrennt verglichen werden.</summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>Prüft Kurzname/FQDN-Kompatibilität und Ablehnung eines fremden Restore-Zielservers.</summary>
|
|
private static void RestoreRejectsDifferentServer()
|
|
{
|
|
var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted);
|
|
snapshot.Server = "BIZTALK-A.example.local";
|
|
SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-A");
|
|
Expect<InvalidOperationException>(() => SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-B"));
|
|
}
|
|
|
|
/// <summary>Prüft, dass ein erhaltener 2.1.3-Snapshot ohne Versionssperre als Recovery-Quelle dient.</summary>
|
|
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<BizTalkSnapshot>(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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft die sichere Restore-Reihenfolge und den Schutz gebundener Orchestrierungen.</summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>Prüft die globale Shutdown-Reihenfolge auch über mehrere Anwendungen hinweg.</summary>
|
|
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 checkpoint = plan.Steps.FindIndex(x => x.Kind == OperationStepKind.OperatorCheckpoint.ToString());
|
|
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 < checkpoint && checkpoint < firstOrchestration, "operator checkpoint was not placed directly after the global receive-location phase");
|
|
Assert(lastOrchestration < firstSendPort, "orchestrations were not globally before send ports");
|
|
Assert(lastSendPort < firstHost, "host instances were not globally last");
|
|
}
|
|
|
|
/// <summary>Prüft den Checkpoint auch dann, wenn keine aktive Receive Location als Planzeile existiert.</summary>
|
|
private static void ShutdownPlanRequiresCheckpointBeforeLaterWorkWithoutReceiveLocations()
|
|
{
|
|
var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted);
|
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
|
var checkpoint = plan.Steps.FindIndex(x => x.Kind == OperationStepKind.OperatorCheckpoint.ToString());
|
|
var sendPort = plan.Steps.FindIndex(x => x.Kind == OperationStepKind.SendPort.ToString());
|
|
|
|
Assert(checkpoint >= 0, "later shutdown work did not create an inbound-drain checkpoint");
|
|
Assert(checkpoint < sendPort, "checkpoint was not placed before later shutdown work");
|
|
}
|
|
|
|
/// <summary>Prüft, dass Scheduler-Erkennung auch nach Planpersistenz möglich bleibt.</summary>
|
|
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<OperationPlan>(path);
|
|
Assert(loaded.Steps.Single(x => x.Kind == "ReceiveLocation").Address.StartsWith("scheduler:", StringComparison.OrdinalIgnoreCase), "scheduler metadata did not survive serialization");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft die ExplorerOM-basierte Zuordnung aller Laufzeitartefakte und den Ambiguitätsschutz.</summary>
|
|
private static void ApplicationResolverMapsArtifactsAndRejectsAmbiguity()
|
|
{
|
|
var resolver = new BizTalkApplicationResolver();
|
|
resolver.AddSendPort("APP-A", "SP-A");
|
|
resolver.AddReceivePort("APP-B", "RP-B");
|
|
resolver.AddReceiveLocation("APP-B", "RL-B");
|
|
resolver.AddOrchestration("APP-C", "Company.Process.Order");
|
|
|
|
Assert(resolver.ApplicationCount == 3, "application count did not de-duplicate artifact owners");
|
|
Assert(resolver.ResolveSendPort("sp-a") == "APP-A", "send port was not resolved case-insensitively");
|
|
Assert(resolver.ResolveReceiveLocation("RL-B", null) == "APP-B", "receive location was not resolved");
|
|
Assert(resolver.ResolveReceiveLocation("missing", "rp-b") == "APP-B", "receive-port fallback was not used");
|
|
Assert(resolver.ResolveOrchestration("company.process.order") == "APP-C", "orchestration was not resolved");
|
|
|
|
resolver.AddSendPort("APP-D", "SP-A");
|
|
Assert(resolver.ResolveSendPort("SP-A") == null, "cross-application duplicate was not rejected as ambiguous");
|
|
}
|
|
|
|
/// <summary>Prüft Dateinamen- und vollständige Assemblyidentität vor prozesslokalem Laden.</summary>
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft Neustart-Rehydration, Vortagskompression und die 30-Tage-Grenze.</summary>
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft, dass erst ein real beschreibbarer Pfad als aktiv gilt.</summary>
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft automatisches Failover, wenn der aktive Pfad später ausfällt.</summary>
|
|
private static void OperationLogFailsOverAfterAppendFailure()
|
|
{
|
|
InTemp(directory =>
|
|
{
|
|
var primary = Path.Combine(directory, "primary");
|
|
var fallback = Path.Combine(directory, "fallback");
|
|
var visible = new List<LogEntry>();
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft eine sichtbare Warnung, wenn kein Dateipfad beschreibbar ist.</summary>
|
|
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<LogEntry>();
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft Typ, HRESULT, innere Ausnahme und Stacktrace für Supportfälle.</summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>Prüft den vollständigen Shutdown-Fortgang von Receive Location bis Host Instance nach einem frühen Fehler.</summary>
|
|
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 checkpointSawReceiveFailure = false;
|
|
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime, (checkpoint, progress) =>
|
|
{
|
|
checkpointSawReceiveFailure = progress.FailedCount == 1;
|
|
return true;
|
|
});
|
|
|
|
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");
|
|
Assert(checkpointSawReceiveFailure, "checkpoint did not receive the completed receive-location failure summary");
|
|
Assert(report.CheckpointDecision == "Continue" && report.Steps.Single(x => x.Kind == OperationStepKind.OperatorCheckpoint.ToString()).Outcome == OperationStepOutcomes.Confirmed, "confirmed drain checkpoint was not persisted");
|
|
}
|
|
|
|
/// <summary>Prüft, dass Nein am Drain-Checkpoint keine spätere Shutdown-Mutation zulässt.</summary>
|
|
private static void ShutdownCheckpointDeclineStopsLaterPhases()
|
|
{
|
|
var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted);
|
|
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RL", 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();
|
|
|
|
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime, (checkpoint, progress) => false);
|
|
|
|
Assert(runtime.Calls.SequenceEqual(new[] { "RL" }), "a later shutdown phase executed after checkpoint decline");
|
|
Assert(report.OperatorStopped && report.CheckpointDecision == "Stop", "operator stop decision was not persisted");
|
|
Assert(report.NotExecutedCount == 3, "not-executed downstream count is wrong");
|
|
Assert(report.Steps.SkipWhile(x => x.Kind != OperationStepKind.OperatorCheckpoint.ToString()).Skip(1).All(x => x.Outcome == OperationStepOutcomes.NotExecuted), "downstream rows were not marked NotExecuted");
|
|
Assert(report.RequiresOperatorReview && !report.HasFailures, "safe operator stop was incorrectly classified as a runtime failure");
|
|
InTemp(directory =>
|
|
{
|
|
var path = Path.Combine(directory, "shutdown-result.json");
|
|
JsonFileStore.Save(path, report);
|
|
var loaded = JsonFileStore.Load<OperationExecutionReport>(path);
|
|
Assert(loaded.OperatorStopped && loaded.CheckpointDecision == "Stop" && loaded.NotExecutedCount == 3, "serialized report lost checkpoint decision evidence");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft den sicheren Stopp, wenn der echte Lauf keinen Dialog-Handler besitzt.</summary>
|
|
private static void ShutdownCheckpointMissingHandlerFailsClosed()
|
|
{
|
|
var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted);
|
|
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RL", Enabled = true });
|
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
|
var runtime = new FakeOperationStepRuntime();
|
|
|
|
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime);
|
|
|
|
Assert(runtime.Calls.SequenceEqual(new[] { "RL" }), "missing checkpoint handler did not fail closed");
|
|
Assert(report.OperatorStopped && report.CheckpointDecision == "Error" && report.FailedCount == 1, "checkpoint handler failure was not durable");
|
|
Assert(report.NotExecutedCount == 1 && report.Steps.Last().Outcome == OperationStepOutcomes.NotExecuted, "later send port was not protected after checkpoint error");
|
|
}
|
|
|
|
/// <summary>Prüft, dass Dry-run den Checkpoint zeigt, aber keine Bedienerentscheidung anfordert.</summary>
|
|
private static void ShutdownCheckpointDryRunDoesNotPrompt()
|
|
{
|
|
var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted);
|
|
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RL", Enabled = true });
|
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
|
var callbackCalls = 0;
|
|
|
|
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(true), null, (checkpoint, progress) => { callbackCalls++; return false; });
|
|
|
|
Assert(callbackCalls == 0, "dry-run requested a real operator checkpoint decision");
|
|
Assert(report.DryRunCount == plan.Steps.Count(x => x.Execute), "dry-run did not display every executable plan row");
|
|
Assert(!report.OperatorStopped && string.IsNullOrWhiteSpace(report.CheckpointDecision), "dry-run persisted a real checkpoint decision");
|
|
}
|
|
|
|
/// <summary>Prüft, dass Dry-run-Hinweis und echter Entscheidungsdialog nicht verwechselt werden können.</summary>
|
|
private static void ShutdownOperatorPromptsDistinguishDryRunAndRealDecision()
|
|
{
|
|
var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted);
|
|
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RL", Enabled = true });
|
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
|
var dryRun = ShutdownOperatorPrompts.BuildDryRunNotice(plan, @"C:\evidence\shutdown-plan.json");
|
|
var prepared = ShutdownOperatorPrompts.BuildPreparedPlanConfirmation("Shutdown", plan, snapshot.Server, @"C:\evidence\shutdown-plan.json");
|
|
var progress = new OperationExecutionReport();
|
|
progress.Steps.Add(new OperationStepResult { Kind = OperationStepKind.ReceiveLocation.ToString(), Outcome = OperationStepOutcomes.Succeeded });
|
|
progress.Steps.Add(new OperationStepResult { Kind = OperationStepKind.ReceiveLocation.ToString(), Outcome = OperationStepOutcomes.Failed });
|
|
var checkpoint = ShutdownOperatorPrompts.BuildInboundDrainConfirmation(progress);
|
|
|
|
Assert(dryRun.Contains("No receive location is disabled") && dryRun.Contains("will not open"), "dry-run notice does not explain the missing real dialog");
|
|
Assert(prepared.Contains("second, explicit decision"), "real-plan confirmation does not announce the later checkpoint dialog");
|
|
Assert(checkpoint.Contains("shutdown is PAUSED") && checkpoint.Contains("Disabled successfully: 1") && checkpoint.Contains("Failed: 1"), "checkpoint decision does not expose its blocking state and receive results");
|
|
Assert(checkpoint.Contains("closing this dialog = stop safely"), "checkpoint close behavior is not explicit");
|
|
}
|
|
|
|
/// <summary>Prüft ENTSSO als erste Voraussetzung des Emergency Restore.</summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>Prüft, dass Kurzname und FQDN dieselbe Host Instance nicht fälschlich überspringen.</summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>Prüft, dass eine Scheduler-artige Exception spätere Schritte nicht mehr verhindert.</summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>Prüft die Fortsetzung auch bei mehreren unterschiedlichen, nicht adapterspezifischen Exceptions.</summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>Prüft, dass selbst eine defekte GUI-Logweiterleitung keine fachlichen Schritte verhindert.</summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>Prüft idempotentes Überspringen eines bereits erreichten Sollzustands.</summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>Prüft die dauerhafte Serialisierung eines Teilfehler-Reports.</summary>
|
|
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<OperationExecutionReport>(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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft die Neutralisierung formelartiger CSV-Feldwerte.</summary>
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft, dass eine nach Manifestbildung veränderte Payload abgelehnt wird.</summary>
|
|
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<InvalidDataException>(() => PackageManifest.ValidateAndRead(app, manifest));
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft die Aktivierung einer vollständig validierten Neuinstallation.</summary>
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft die erfolgreiche Aktivierung nach einer kurzzeitigen Rename-Sperre.</summary>
|
|
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<int>();
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft den verifizierten Kopierfallback einer durchgehend gesperrten Neuinstallation.</summary>
|
|
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<int>();
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft den verifizierten Kopierfallback, nachdem eine Update-Vorversion atomar gesichert wurde.</summary>
|
|
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<int>();
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft das Backup-Rollback, wenn der Ziel-Self-Test nach einem Update-Kopierfallback fehlschlägt.</summary>
|
|
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<InvalidOperationException>(() => 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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft, dass ein Update bei dauerhaft gesperrtem Backup atomar und unverändert abbricht.</summary>
|
|
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<int>();
|
|
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<InvalidOperationException>(() => 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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft die Ablehnung nicht deklarierter Dateien und ausbrechender Manifestpfade.</summary>
|
|
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<InvalidDataException>(() => 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<InvalidDataException>(() => PackageManifest.ValidateAndRead(app, manifest));
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft, dass ein Staging-Fehler die aktive Installation nicht mutiert.</summary>
|
|
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<string>();
|
|
var engine = new InstallerEngine(package, install, data, false, path => false);
|
|
var exception = Capture<InvalidOperationException>(() => 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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft die Wiederherstellung der Vorversion nach fehlgeschlagenem aktiviertem Self-Test.</summary>
|
|
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<InvalidOperationException>(() => 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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft die Deinstallation über ein atomar umbenanntes Quarantäneverzeichnis.</summary>
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft technischen Kontext, Fehlercode, HRESULT und innere Ausnahme im Setup-Log.</summary>
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft, dass ein fehlerhafter UI-Callback die Installation nicht beeinflusst.</summary>
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft, dass optionale Desktop- und Warnungsfehler niemals eine Kernoperation abbrechen.</summary>
|
|
private static void OptionalWindowsIntegrationFailuresAreNonFatal()
|
|
{
|
|
InTemp(directory =>
|
|
{
|
|
var log = SetupOperationLog.Create(directory, "optional-integration-test");
|
|
var warnings = new List<string>();
|
|
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");
|
|
});
|
|
}
|
|
|
|
/// <summary>Prüft den Diagnose-Log-Fallback bei einem nicht verwendbaren ProgramData-Pfad.</summary>
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>Erstellt einen minimalen ausführbaren Testplan.</summary>
|
|
/// <param name="names">Die Namen der Testschritte in Ausführungsreihenfolge.</param>
|
|
/// <returns>Ein Restore-Testplan.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Erstellt konsistente Optionen für portable Executor-Tests.</summary>
|
|
/// <param name="dryRun">True für einen simulierenden Lauf.</param>
|
|
/// <returns>Testoptionen.</returns>
|
|
private static OperationOptions TestOptions(bool dryRun)
|
|
{
|
|
return new OperationOptions
|
|
{
|
|
Server = Environment.MachineName,
|
|
OutputDirectory = Path.GetTempPath(),
|
|
StateFile = "before.json",
|
|
DryRun = dryRun,
|
|
WaitTimeoutSeconds = 30,
|
|
PollIntervalSeconds = 1
|
|
};
|
|
}
|
|
|
|
/// <summary>Simuliert bereits erreichte Zustände, Mutationen und isolierte Laufzeitfehler.</summary>
|
|
private sealed class FakeOperationStepRuntime : IOperationStepRuntime
|
|
{
|
|
/// <summary>Initialisiert die Aufruf- und Mutationslisten.</summary>
|
|
public FakeOperationStepRuntime()
|
|
{
|
|
Calls = new List<string>();
|
|
MutatedNames = new List<string>();
|
|
AlreadySatisfiedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <summary>Gets or sets the step name that throws the simulated Scheduler error.</summary>
|
|
public string FailingName { get; set; }
|
|
|
|
/// <summary>Gets every runtime call in order.</summary>
|
|
public List<string> Calls { get; private set; }
|
|
|
|
/// <summary>Gets only steps that required a simulated mutation.</summary>
|
|
public List<string> MutatedNames { get; private set; }
|
|
|
|
/// <summary>Gets the names reported as already satisfied.</summary>
|
|
public HashSet<string> AlreadySatisfiedNames { get; private set; }
|
|
|
|
/// <summary>Simuliert einen zustandsbewussten Laufzeitschritt.</summary>
|
|
/// <param name="step">Der Testschritt.</param>
|
|
/// <param name="options">Die ungenutzten Testoptionen.</param>
|
|
/// <returns>Das simulierte Ergebnis.</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Wirft verschiedene Exception-Typen und liefert danach wieder ein erfolgreiches Ergebnis.</summary>
|
|
private sealed class MultipleFailureOperationStepRuntime : IOperationStepRuntime
|
|
{
|
|
/// <summary>Initialisiert die vollständige Aufrufliste.</summary>
|
|
public MultipleFailureOperationStepRuntime()
|
|
{
|
|
Calls = new List<string>();
|
|
}
|
|
|
|
/// <summary>Gets all attempted step names.</summary>
|
|
public List<string> Calls { get; private set; }
|
|
|
|
/// <summary>Wirft je nach Testschritt eine andere unerwartete Ausnahme.</summary>
|
|
/// <param name="step">Der auszuführende Testschritt.</param>
|
|
/// <param name="options">Die ungenutzten Testoptionen.</param>
|
|
/// <returns>Erfolg für den abschließenden Fortsetzungsschritt.</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Erstellt einen minimalen Snapshot für Vergleiche und Planprüfungen.</summary>
|
|
/// <param name="application">Der Name der Testanwendung.</param>
|
|
/// <param name="port">Der Name des Test-Send-Ports.</param>
|
|
/// <param name="state">Der rohe Send-Port-Status.</param>
|
|
/// <returns>Ein Snapshot mit einer Anwendung und einem Send Port.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Erzeugt eine minimale, manifestierte Installer-Payload.</summary>
|
|
/// <param name="root">Das temporäre Testwurzelverzeichnis.</param>
|
|
/// <param name="payload">Der simulierte Inhalt der Anwendungs-EXE.</param>
|
|
/// <returns>Das Verzeichnis des erzeugten Testpakets.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Führt einen Test in einem eindeutigen temporären Verzeichnis mit garantierter Bereinigung aus.</summary>
|
|
/// <param name="action">Die Testfunktion, die den temporären Pfad erhält.</param>
|
|
private static void InTemp(Action<string> 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); }
|
|
}
|
|
|
|
/// <summary>Bricht den Test ab, wenn eine erwartete Bedingung nicht erfüllt ist.</summary>
|
|
/// <param name="condition">Die erwartete Bedingung.</param>
|
|
/// <param name="message">Die Fehlermeldung bei nicht erfüllter Bedingung.</param>
|
|
private static void Assert(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); }
|
|
|
|
/// <summary>Prüft, dass eine Aktion eine bestimmte Ausnahme auslöst.</summary>
|
|
/// <typeparam name="T">Der erwartete Ausnahmetyp.</typeparam>
|
|
/// <param name="action">Die auszuführende Aktion.</param>
|
|
private static void Expect<T>(Action action) where T : Exception
|
|
{
|
|
try { action(); }
|
|
catch (T) { return; }
|
|
throw new InvalidOperationException("Expected exception " + typeof(T).Name);
|
|
}
|
|
|
|
/// <summary>Führt eine Aktion aus und gibt die erwartete Ausnahme für weitere Prüfungen zurück.</summary>
|
|
/// <typeparam name="T">Der erwartete Ausnahmetyp.</typeparam>
|
|
/// <param name="action">Die auszuführende Aktion.</param>
|
|
/// <returns>Die von der Aktion ausgelöste Ausnahme.</returns>
|
|
private static T Capture<T>(Action action) where T : Exception
|
|
{
|
|
try { action(); }
|
|
catch (T ex) { return ex; }
|
|
throw new InvalidOperationException("Expected exception " + typeof(T).Name);
|
|
}
|
|
}
|
|
}
|