using System.IO.Compression;
using System.Security.Cryptography;
using SQLInsight.Licensing;
using SQLInsight.Rules.Lifecycle;
namespace SQLInsight.Updates.Tests;
/// A feed folder signed with a throwaway key, plus a data folder and an installation to update.
public sealed class TestFeed : IDisposable
{
public const string KeyId = "test";
private readonly ECDsa _key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
public TestFeed()
{
Root = Path.Combine(Path.GetTempPath(), "sqlinsight-updates-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(FeedDirectory);
Directories = new UpdateDirectories(Path.Combine(Root, "data"));
Keys = new TrustedKeys([KeyValuePair.Create(KeyId, Convert.ToBase64String(_key.ExportSubjectPublicKeyInfo()))]);
Feed = new UpdateFeed(FeedDirectory, null);
}
public string Root { get; }
public string FeedDirectory => Path.Combine(Root, "feed");
public string AgentFolder => Path.Combine(Root, "install", "Agent");
public UpdateDirectories Directories { get; }
public TrustedKeys Keys { get; }
public UpdateFeed Feed { get; }
public FileRef AddFile(string path, byte[] bytes)
{
var full = Path.Combine(FeedDirectory, path);
Directory.CreateDirectory(Path.GetDirectoryName(full)!);
File.WriteAllBytes(full, bytes);
return new FileRef(path, Convert.ToHexString(SHA256.HashData(bytes)), bytes.Length);
}
public string Sign(UpdateManifest manifest) => SignedDocument.Sign(manifest, _key);
public UpdateManifest Publish(string channel = "stable", RulePackRef? rulePack = null, AppRelease? release = null, DateTimeOffset? publishedAt = null, DateTimeOffset? expiresAt = null)
{
var manifest = new UpdateManifest
{
KeyId = KeyId,
Channel = channel,
PublishedAt = publishedAt ?? DateTimeOffset.UtcNow,
ExpiresAt = expiresAt ?? DateTimeOffset.UtcNow.AddDays(30),
RulePack = rulePack,
Release = release,
};
var path = Path.Combine(FeedDirectory, channel, UpdateFeed.ManifestFile);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, Sign(manifest));
return manifest;
}
public RulePackRef RulePack(Version version)
{
var root = RepositoryRoot();
var bytes = Rules.Lifecycle.RulePack.Create(version,
File.ReadAllBytes(Path.Combine(root, "src", "SQLInsight.Rules", "Lifecycle", "build-catalog.json")),
File.ReadAllBytes(Path.Combine(root, "src", "SQLInsight.Rules", "Lifecycle", "support-lifecycle.json")));
return new RulePackRef(version.ToString(), AddFile($"rulepacks/rulepack-{version}.json", bytes));
}
/// A release whose Agent folder holds the given program file content.
public AppRelease Release(string version, string program = "v2", string? platform = null)
{
using var buffer = new MemoryStream();
using (var zip = new ZipArchive(buffer, ZipArchiveMode.Create, leaveOpen: true))
{
Add(zip, "Agent/SQLInsight.Agent.dll", program);
Add(zip, "Agent/appsettings.json", "{ \"from\": \"package\" }");
Add(zip, "Agent/runtimes/new.dll", "new");
Add(zip, "Web/SQLInsight.Web.dll", "web");
}
return new AppRelease
{
Version = version,
Released = new DateOnly(2026, 10, 1),
Assets = [new ReleaseAsset(platform ?? Platform.Current, AddFile($"releases/sqlinsight-{version}.zip", buffer.ToArray()))],
};
}
/// A release published as an MSI for installations made with the MSI.
public AppRelease MsiRelease(string version) => new()
{
Version = version,
Released = new DateOnly(2026, 10, 1),
Assets =
[
new ReleaseAsset("win-x64", AddFile($"releases/sqlinsight-{version}-win-x64.zip", [1, 2, 3])),
new ReleaseAsset("win-x64-msi", AddFile($"releases/SQLInsight-Pro-{version}-x64.msi", [0xD0, 0xCF, 0x11, 0xE0, 4, 5, 6])),
],
};
public void Install(string program = "v1")
{
Directory.CreateDirectory(AgentFolder);
File.WriteAllText(Path.Combine(AgentFolder, "SQLInsight.Agent.dll"), program);
File.WriteAllText(Path.Combine(AgentFolder, "appsettings.json"), "{ \"from\": \"customer\" }");
}
public UpdateChecker Checker(string channel = "stable", string? platform = null) => new(Feed, Keys, Directories, channel, TimeProvider.System, platform ?? Platform.Current);
public void Dispose()
{
_key.Dispose();
if (Directory.Exists(Root))
{
Directory.Delete(Root, recursive: true);
}
}
private static void Add(ZipArchive zip, string name, string content)
{
using var writer = new StreamWriter(zip.CreateEntry(name).Open());
writer.Write(content);
}
private static string RepositoryRoot()
{
for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent)
{
if (File.Exists(Path.Combine(dir.FullName, "SQLInsight.slnx")))
{
return dir.FullName;
}
}
throw new InvalidOperationException("Repository root not found.");
}
}
/// Services that "crash" when the installed program file says so.
public sealed class FakeServices(string programFile) : IServiceControl
{
public List Calls { get; } = [];
public Task StopAsync(string service, CancellationToken cancellationToken)
{
Calls.Add("stop " + service);
return Task.CompletedTask;
}
public Task StartAsync(string service, CancellationToken cancellationToken)
{
Calls.Add("start " + service);
return Task.CompletedTask;
}
public Task IsRunningAsync(string service, CancellationToken cancellationToken) =>
Task.FromResult(File.ReadAllText(programFile) != "broken");
}
public sealed class FakeMsi(int exitCode) : IMsiInstaller
{
public List<(string Msi, byte[] Content)> Installed { get; } = [];
public Task InstallAsync(string msiPath, string logPath, CancellationToken cancellationToken)
{
Installed.Add((msiPath, File.ReadAllBytes(msiPath)));
return Task.FromResult(exitCode);
}
}