Support ScheduledTask control and persistent logs

This commit is contained in:
2026-08-26 10:23:45 +02:00
parent 3b4a621cd3
commit 999f69bcb9
24 changed files with 1091 additions and 84 deletions
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using BizTalkPlatformManagementTool.Models;
using BizTalkPlatformManagementTool.Services;
@@ -35,6 +36,10 @@ namespace BizTalkPlatformManagementTool.Tests
Run("LoggerSinkFailureCannotAbortPlanExecution", LoggerSinkFailureCannotAbortPlanExecution);
Run("PlanExecutionSkipsAlreadySatisfiedState", PlanExecutionSkipsAlreadySatisfiedState);
Run("ExecutionReportRoundTripPreservesFailure", ExecutionReportRoundTripPreservesFailure);
Run("ScheduledReceivePlanCarriesAdapterMetadata", ScheduledReceivePlanCarriesAdapterMetadata);
Run("AdapterAssemblyResolverRequiresMatchingIdentity", AdapterAssemblyResolverRequiresMatchingIdentity);
Run("OperationLogPersistsCompressesAndRetainsThirtyDays", OperationLogPersistsCompressesAndRetainsThirtyDays);
Run("ExceptionDiagnosticsContainSupportContext", ExceptionDiagnosticsContainSupportContext);
Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues);
Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering);
Run("PackageManifestRejectsUndeclaredAndTraversalFiles", PackageManifestRejectsUndeclaredAndTraversalFiles);
@@ -169,6 +174,96 @@ namespace BizTalkPlatformManagementTool.Tests
Assert(lastSendPort < firstHost, "host instances were not globally last");
}
/// <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 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 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()
{