Skip to content
← All posts
· Delta1 Labs Licensing.NETGuide

Testing the code that lives behind a license check

Your paid features sit behind a license gate — but how do you test them without a live issuing service, a real key, or the wall clock? Here is a two-layer strategy for Keyright: a thin seam you mock for fast unit tests, and a handful of committed, test-key-signed fixtures that exercise the real SDK verification path fully offline and deterministically in CI.

Somewhere in your app is a branch that reads, in effect, “if this customer is licensed, do the valuable thing.” That branch is the whole point of shipping a licensed product — and it is also the branch your tests most often skip, because exercising it seems to require a license server, a real key and, for the expiry cases, a time machine. So the export button, the seat-limit prompt, the enterprise-only pane go untested, and the first time anyone runs the licensed path end to end is in front of a paying customer.

It doesn’t have to be that way. This post lays out a two-layer strategy for testing Keyright-gated code: a thin seam you mock for the fast majority of your tests, and a few committed, test-key-signed fixtures that drive the real SDK verification path — fully offline, with no service in the loop and no dependence on the clock.

Why the naive approaches hurt

Two instincts both end badly. The first is to let the tests talk to your real issuing service: now your unit suite needs the network, a valid key, and a seat it burns on every run, and it goes red whenever the service is down. The second is to not test the gated code at all and “verify it manually” — which means never.

The fix is to notice that there are really two different things you want to test, and they want different tools:

  • Does my feature behave correctly given a licensing outcome? (Is the export button hidden on Free? Does the seat prompt fire at the limit?) This is the bulk of your tests, and it should be pure, fast and have nothing to do with cryptography.
  • Does the SDK resolve a given license into the outcome I expect? (Does this Pro key actually unlock? Does a tampered key fail closed?) This is a handful of tests, and it should exercise the real verification code — just without a server.
Your feature codedepends on ILicenseGateILicenseGateLayer 1 — most testsFakeLicenseGate (no crypto)inject any edition / entitlementfast · offline · deterministicLayer 2 — a few testsreal KeyrightClientcommitted test-key fixturesreal verification · no server

Layer 1: a seam you can mock

The Keyright .NET SDK is deliberately small — Validate(), IsEnabled, GetLimit — and it never throws, which already makes it pleasant to call. But your view-models and command handlers should not call it directly, because then every test of them drags in the real client. Put a one-method-per-question interface in front of it:

public interface ILicenseGate
{
    bool IsPaid { get; }
    bool IsEnabled(string feature);
    long GetLimit(string feature, long fallback);
}

The production implementation is a thin wrapper over the single KeyrightClient you built at startup:

public sealed class KeyrightLicenseGate : ILicenseGate
{
    private readonly KeyrightClient _client;
    public KeyrightLicenseGate(KeyrightClient client) => _client = client;

    public bool IsPaid => _client.Validate().IsPaid;
    public bool IsEnabled(string feature) => _client.IsEnabled(feature);
    public long GetLimit(string feature, long fallback) => _client.GetLimit(feature, fallback);
}

Your feature code takes ILicenseGate, never KeyrightClient:

public sealed class ExportViewModel
{
    private readonly ILicenseGate _license;
    public ExportViewModel(ILicenseGate license) => _license = license;

    public bool CanExport => _license.IsEnabled("export");
    public int  MaxRows   => (int)_license.GetLimit("max-rows", fallback: 100);
}

Now the fake is trivial — and, importantly, it can express any licensing state instantly, including the ones that are painful to produce for real:

public sealed class FakeLicenseGate : ILicenseGate
{
    private readonly Dictionary<string, string> _ent = new();
    public bool IsPaid { get; init; }

    public FakeLicenseGate With(string feature, string value) { _ent[feature] = value; return this; }

    public bool IsEnabled(string feature) => _ent.TryGetValue(feature, out var v) && v == "true";
    public long GetLimit(string feature, long fallback)
        => _ent.TryGetValue(feature, out var v) && long.TryParse(v, out var n) ? n : fallback;
}

The tests read like specifications, run in microseconds, and touch no key material:

[Fact]
public void Export_is_hidden_on_the_free_edition()
{
    var vm = new ExportViewModel(new FakeLicenseGate { IsPaid = false });
    Assert.False(vm.CanExport);
}

[Fact]
public void Export_unlocks_when_the_entitlement_is_present()
{
    var gate = new FakeLicenseGate { IsPaid = true }.With("export", "true").With("max-rows", "50000");
    var vm = new ExportViewModel(gate);
    Assert.True(vm.CanExport);
    Assert.Equal(50000, vm.MaxRows);
}

This is where the overwhelming majority of your licensing-adjacent tests belong. Note that FakeLicenseGate mirrors Keyright’s own wire convention — entitlement values are strings, "true"/"false" for flags and an integer (or "unlimited") for limits — so a fake that returns "true" tests exactly what the real gate returns.

Layer 2: real verification against committed fixtures

Layer 1 trusts that KeyrightLicenseGate maps the SDK correctly. A second, much smaller layer proves that the SDK itself turns a real license into the edition and entitlements you expect — and it can do so with no server, because Keyright is offline-first: Validate() verifies a signature against an embedded public key and needs no network.

The trick is to treat license keys as test fixtures. Create a dedicated throwaway test product (call it acme-test, ideally in a non-production workspace), issue a few keys once — a Pro key with known entitlements, an Enterprise key, and a key for a different product — and commit the key strings together with that test product’s public key. None of this is sensitive: the public key is already shipped in every binary and can only verify, and the fixtures are signed claims unrelated to your production key or any real customer.

Feed a fixture straight into a real client through the LicenseString source — the highest-precedence source, so it wins with no file and no cache involved:

public static class Fixtures
{
    // Public key of the throwaway "acme-test" product. NOT a secret — safe to commit.
    public const string TestPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...";

    public static LicenseInfo Resolve(string licenseString) =>
        KeyrightClient.Initialize(new KeyrightOptions
        {
            Product         = "acme-test",
            PublicKeyBase64 = TestPublicKey,
            LicenseString   = licenseString,   // feed the fixture directly: no file, no network, no cache
        }).Validate();
}

Now the real verification path is under test, and every case is deterministic:

[Fact]
public void Pro_fixture_resolves_to_a_paid_edition_with_its_entitlements()
{
    LicenseInfo info = Fixtures.Resolve(File.ReadAllText("fixtures/pro.key"));

    Assert.True(info.IsPaid);
    Assert.True(info.Entitlements.IsEnabled("export"));
    Assert.Equal(10, info.Entitlements.GetLimit("max-projects", fallback: 1));
}

[Fact]
public void A_tampered_key_fails_closed_to_free_rather_than_throwing()
{
    // Flip one character to break the signature.
    string tampered = File.ReadAllText("fixtures/pro.key").Replace('A', 'B');

    LicenseInfo info = Fixtures.Resolve(tampered);

    Assert.False(info.IsPaid);                              // dropped to Free, not an exception
    Assert.Equal("SignatureInvalid", info.Status.ToString());
}

[Fact]
public void A_key_for_a_different_product_does_not_unlock_this_one()
{
    LicenseInfo info = Fixtures.Resolve(File.ReadAllText("fixtures/other-product.key"));
    Assert.False(info.IsPaid);
}

The tampered-key test is the one that pays for the whole layer: it proves your build fails closed. If a future SDK bump or a mis-set option ever made a bad signature resolve to a paid edition, this test goes red in CI long before it ships. You can test the source-precedence wiring the same way — drop a fixture on disk and set LicenseFilePath, or point the KEYRIGHT_LICENSE environment variable at it, to prove your bootstrap reads the license from where you think it does.

What deliberately stays out of the fast suite

Three behaviours are real, important, and wrong to force into a unit test: expiry, revocation, and clock-tamper detection. They are time- and network-dependent — a genuinely expired lease, a revocation that propagates on the next refresh, a clock rolled backward past the tolerance — and faking them convincingly at the SDK level means reproducing server state you shouldn’t mock.

Test them at the two levels that fit. At Layer 1, assert how your app reacts when the gate reports those states (inject a fake whose IsPaid is false and whose status is Expired or Revoked, and check that you show the right “renew” prompt). And verify the real transitions once, by hand or in a scripted job, against your test tenant — the exact sequence is the end-to-end checklist in the .NET SDK guide: activate, pull the network and confirm the cached lease still validates, then revoke and confirm the next refresh drops to Free. That belongs in a nightly or pre-release job, not in the suite that gates every commit.

The payoff

The split keeps each test honest about what it is really checking. Layer 1 makes your licensed code paths as testable as any other branch — fast, deterministic, and free of key material — so “hidden on Free, unlocked on Pro” stops being something you verify by hand. Layer 2 nails the one thing Layer 1 assumes, that the SDK maps a real signed license to the right outcome and fails closed on a bad one, and it does so with nothing but committed fixtures and the offline verifier. Wire the gate at startup as in the .NET SDK integration guide, read how leases and offline validation behave on the security & leases page, and the free plan is more than enough to mint the handful of fixtures this strategy runs on.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.