Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Successful in 1m1s
deploy hub / deploy (push) Successful in 19s
CI / MSI-installer (push) Successful in 36s
- src/SQLInsight.Community: offentligt websted med forside, funktioner, download af signerede udgivelser (med SHA-256), dokumentation fra docs/, SQL Server- versioner fra den nyeste signerede regelpakke, regler og supportaftaler. Viser de seneste forumemner. Streng CSP, ingen login, ingen database. - deploy/forum: Discourse-opsætning (app.yml til discourse_docker, dansk sprog og søgning), install.sh og configure.py, der idempotent opsætter kategorier, løste svar, velkomstemne og indstillinger via API'et. - deploy/community: Dockerfile og docker compose med Caddy til TLS. - Webbrugerfladen linker til forum, dokumentation og support på webstedet (SQLInsight:Web:ForumUrl og CommunityUrl). - Udgivelsesfiler hedder sqlinsight-<version>-<platform>.<ext>. - Tests: sider, 404, signerede og manipulerede feeds, forum (også nede), dokumentationslinks og regler. - docs/community.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
170 lines
6.6 KiB
C#
170 lines
6.6 KiB
C#
using System.IO.Compression;
|
|
using System.Security.Cryptography;
|
|
using SQLInsight.Licensing;
|
|
using SQLInsight.Rules.Lifecycle;
|
|
|
|
namespace SQLInsight.Updates.Tests;
|
|
|
|
/// <summary>A feed folder signed with a throwaway key, plus a data folder and an installation to update.</summary>
|
|
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; }
|
|
|
|
/// <summary>The throwaway key's public half (base64 SubjectPublicKeyInfo), as configured in TrustedKeys.</summary>
|
|
public string PublicKey => Convert.ToBase64String(_key.ExportSubjectPublicKeyInfo());
|
|
|
|
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));
|
|
}
|
|
|
|
/// <summary>A release whose Agent folder holds the given program file content.</summary>
|
|
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()))],
|
|
};
|
|
}
|
|
|
|
/// <summary>A release published as an MSI for installations made with the MSI.</summary>
|
|
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.");
|
|
}
|
|
}
|
|
|
|
/// <summary>Services that "crash" when the installed program file says so.</summary>
|
|
public sealed class FakeServices(string programFile) : IServiceControl
|
|
{
|
|
public List<string> 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<bool> 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<int> InstallAsync(string msiPath, string logPath, CancellationToken cancellationToken)
|
|
{
|
|
Installed.Add((msiPath, File.ReadAllBytes(msiPath)));
|
|
return Task.FromResult(exitCode);
|
|
}
|
|
}
|