Add transactional installer and harden runtime operations
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BizTalkPlatformManagementTool.Models;
|
||||
using BizTalkPlatformManagementTool.Services;
|
||||
using BizTalkPlatformManagementTool.Setup;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Tests
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private static int failures;
|
||||
|
||||
private static int Main()
|
||||
{
|
||||
Run("JsonRoundTripIsBomTolerantAndAtomic", JsonRoundTripIsBomTolerantAndAtomic);
|
||||
Run("DiffUsesApplicationAndNameIdentity", DiffUsesApplicationAndNameIdentity);
|
||||
Run("RestoreRejectsDifferentServer", RestoreRejectsDifferentServer);
|
||||
Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder);
|
||||
Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues);
|
||||
Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering);
|
||||
Run("PackageManifestRejectsUndeclaredAndTraversalFiles", PackageManifestRejectsUndeclaredAndTraversalFiles);
|
||||
Run("InstallerActivatesValidatedPayload", InstallerActivatesValidatedPayload);
|
||||
Run("InstallerDoesNotMutateOnStagingFailure", InstallerDoesNotMutateOnStagingFailure);
|
||||
Run("InstallerRollsBackFailedActivatedSelfTest", InstallerRollsBackFailedActivatedSelfTest);
|
||||
Run("InstallerUninstallRemovesProgramDirectory", InstallerUninstallRemovesProgramDirectory);
|
||||
Console.WriteLine(failures == 0 ? "ALL TESTS PASSED" : failures + " TEST(S) FAILED");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
private static void Run(string name, Action test)
|
||||
{
|
||||
try { test(); Console.WriteLine("PASS " + name); }
|
||||
catch (Exception ex) { failures++; Console.Error.WriteLine("FAIL " + name + ": " + ex); }
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
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 plan = new BizTalkOperationService(null).CreateRestorePlan(snapshot, snapshot.Server);
|
||||
Assert(plan.Steps.First().Kind == "HostInstance", "host instance must start first");
|
||||
Assert(plan.Steps.Last().Kind == "ReceiveLocation", "receive location must be restored last");
|
||||
var bound = plan.Steps.Single(x => x.Name == "ORCH");
|
||||
Assert(!bound.Execute && bound.Kind == "Note", "bound orchestration must remain unchanged");
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
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 engine = new InstallerEngine(package, install, Path.Combine(directory, "data"), false, path => false);
|
||||
Expect<InvalidOperationException>(() => engine.Install(false, null));
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
Expect<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");
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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); }
|
||||
}
|
||||
|
||||
private static void Assert(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); }
|
||||
private static void Expect<T>(Action action) where T : Exception
|
||||
{
|
||||
try { action(); }
|
||||
catch (T) { return; }
|
||||
throw new InvalidOperationException("Expected exception " + typeof(T).Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user