sqlinsight-pro/tests/SQLInsight.Licensing.Tests/LicenseTests.cs
Søren Eggert Lundsteen Olsen b9afc30436
Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-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:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Successful in 1m13s
deploy community / deploy (push) Successful in 0s
deploy hub / deploy (push) Successful in 20s
CI / MSI-installer (push) Successful in 43s
Ship SEOSoft's license key k20261001; agent defaults to the real hub
- trusted-keys.json: the public key from keygen (k20261001). The hub and
  every agent built from now on verify licenses with it; no
  TrustedKeys configuration is needed any more.
- The test that required an empty key file now checks what it was
  guarding against: every embedded key must be keygen output (id
  k<yyyyMMdd>, P-256 public key), never a test key.
- Agent: default Hub:Url https://hub.sqlinsight.seosoft.dk/ (was
  hub.seosoft.dk, which does not exist).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 07:15:47 +02:00

137 lines
5.1 KiB
C#

using System.Security.Cryptography;
namespace SQLInsight.Licensing.Tests;
public class LicenseTests
{
private static readonly DateTimeOffset Now = new(2026, 9, 29, 12, 0, 0, TimeSpan.Zero);
private sealed class FixedTime(DateTimeOffset now) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => now;
}
private static (ECDsa Key, TrustedKeys Trusted) NewKey(string kid = "test1")
{
var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
return (key, new TrustedKeys([KeyValuePair.Create(kid, Convert.ToBase64String(key.ExportSubjectPublicKeyInfo()))]));
}
private static License Sample(DateTimeOffset expires, LicensePlan plan = LicensePlan.Professional, string kid = "test1") => new()
{
LicenseId = "lic-1", Customer = "Kunde A/S", Plan = plan, IssuedAt = Now.AddDays(-10), ExpiresAt = expires, MaxInstances = 5, KeyId = kid,
};
private static LicenseStatus Check(string text, TrustedKeys keys, DateTimeOffset? now = null) => new LicenseVerifier(keys, new FixedTime(now ?? Now)).Check(text);
[Fact]
public void Valid_license_round_trips()
{
var (key, trusted) = NewKey();
var text = SignedDocument.Sign(Sample(Now.AddYears(1)), key);
var status = Check(text, trusted);
Assert.StartsWith("SQLI1.", text, StringComparison.Ordinal);
Assert.Equal(LicenseState.Valid, status.State);
Assert.Equal("Kunde A/S", status.License!.Customer);
Assert.True(status.AllowsFindingsSync);
}
[Fact]
public void No_license_is_community_and_not_an_error()
{
var status = Check("", NewKey().Trusted);
Assert.Equal(LicenseState.None, status.State);
Assert.False(status.AllowsFindingsSync);
}
[Fact]
public void Tampered_payload_is_invalid()
{
var (key, trusted) = NewKey();
var parts = SignedDocument.Sign(Sample(Now.AddYears(1)), key).Split('.');
var forged = SignedDocument.Sign(Sample(Now.AddYears(10), LicensePlan.Enterprise), ECDsa.Create(ECCurve.NamedCurves.nistP256)).Split('.');
// Enterprise payload with the original, genuine signature.
Assert.Equal(LicenseState.Invalid, Check($"{parts[0]}.{forged[1]}.{parts[2]}", trusted).State);
}
[Fact]
public void License_signed_with_an_untrusted_key_is_invalid()
{
var (_, trusted) = NewKey();
var (other, _) = NewKey();
Assert.Equal(LicenseState.Invalid, Check(SignedDocument.Sign(Sample(Now.AddYears(1)), other), trusted).State);
}
[Fact]
public void Unknown_key_id_is_invalid()
{
var (key, trusted) = NewKey("test1");
Assert.Equal(LicenseState.Invalid, Check(SignedDocument.Sign(Sample(Now.AddYears(1), kid: "other"), key), trusted).State);
}
[Theory]
[InlineData("garbage")]
[InlineData("SQLI1.x.y")]
[InlineData("SQLI1.@@@.###")]
[InlineData("SQLI2.a.b")]
public void Garbage_is_invalid_not_an_exception(string text)
{
Assert.Equal(LicenseState.Invalid, Check(text, NewKey().Trusted).State);
}
[Fact]
public void Expired_license_keeps_syncing_during_grace_then_stops()
{
var (key, trusted) = NewKey();
var text = SignedDocument.Sign(Sample(Now), key);
var grace = Check(text, trusted, Now.AddDays(10));
var expired = Check(text, trusted, Now.AddDays(31));
Assert.Equal(LicenseState.ExpiredInGrace, grace.State);
Assert.True(grace.AllowsFindingsSync);
Assert.Equal(LicenseState.Expired, expired.State);
Assert.False(expired.AllowsFindingsSync);
}
[Fact]
public void Community_plan_does_not_include_sync()
{
var (key, trusted) = NewKey();
Assert.False(Check(SignedDocument.Sign(Sample(Now.AddYears(1), LicensePlan.Community), key), trusted).AllowsFindingsSync);
}
[Fact]
public void Plans()
{
Assert.True(Sample(Now, LicensePlan.Enterprise).IncludesPhoneSupport);
Assert.False(Sample(Now, LicensePlan.Professional).IncludesPhoneSupport);
Assert.True(Sample(Now, LicensePlan.Professional).IncludesPrioritySupport);
}
[Fact]
public void Embedded_keys_are_vendor_public_keys()
{
// Only SEOSoft's keygen public keys may ship. A test key (whose private half is in a repository) would let
// anyone mint licenses, so every entry must look like keygen output: id k<yyyyMMdd> and a P-256 public key.
using var stream = typeof(TrustedKeys).Assembly.GetManifestResourceStream("SQLInsight.Licensing.trusted-keys.json")!;
using var document = System.Text.Json.JsonDocument.Parse(stream);
var keys = document.RootElement.GetProperty("keys").EnumerateArray().ToList();
Assert.Equal(keys.Count, TrustedKeys.Embedded().Count);
foreach (var entry in keys)
{
Assert.Matches(@"^k\d{8}$", entry.GetProperty("id").GetString());
using var key = ECDsa.Create();
key.ImportSubjectPublicKeyInfo(Convert.FromBase64String(entry.GetProperty("publicKey").GetString()!), out _);
Assert.Equal(256, key.KeySize);
}
}
}