Code testen, der hinter einer Lizenzprüfung liegt
Ihre bezahlten Funktionen liegen hinter einem Lizenz-Gate — aber wie testen Sie sie ohne laufenden Lizenzdienst, ohne echten Schlüssel und ohne Abhängigkeit von der Systemuhr? Hier ist eine zweischichtige Strategie für Keyright: eine dünne Nahtstelle, die Sie für schnelle Unit-Tests mocken, und eine Handvoll eingecheckter, mit einem Testschlüssel signierter Fixtures, die den echten SDK-Verifikationspfad vollständig offline und deterministisch in CI durchlaufen.
Irgendwo in Ihrer App gibt es eine Verzweigung, die sinngemäß lautet: „Wenn dieser Kunde lizenziert ist, tu das Wertvolle.“ Diese Verzweigung ist der ganze Sinn eines lizenzierten Produkts — und sie ist zugleich die Verzweigung, die Ihre Tests am häufigsten überspringen, weil ihr Durchlaufen scheinbar einen Lizenzserver, einen echten Schlüssel und, für die Ablauffälle, eine Zeitmaschine erfordert. Also bleiben der Export-Button, die Hinweis-Meldung beim Seat-Limit und das Enterprise-only-Panel ungetestet, und der lizenzierte Pfad wird zum ersten Mal Ende-zu-Ende vor einem zahlenden Kunden ausgeführt.
Das muss nicht so sein. Dieser Beitrag beschreibt eine zweischichtige Strategie zum Testen von Keyright-geschütztem Code: eine dünne Nahtstelle, die Sie für die schnelle Mehrheit Ihrer Tests mocken, und ein paar eingecheckte, mit einem Testschlüssel signierte Fixtures, die den echten SDK-Verifikationspfad antreiben — vollständig offline, ohne Dienst in der Schleife und ohne Abhängigkeit von der Uhr.
Warum die naiven Ansätze wehtun
Zwei Instinkte enden beide schlecht. Der erste: die Tests mit Ihrem echten Lizenzdienst sprechen lassen — jetzt braucht Ihre Unit-Suite Netzwerk, einen gültigen Schlüssel und einen Seat, den sie bei jedem Lauf verbraucht, und sie wird rot, sobald der Dienst nicht erreichbar ist. Der zweite: den geschützten Code gar nicht testen und ihn „manuell verifizieren“ — was bedeutet: nie.
Die Lösung beginnt mit der Einsicht, dass Sie in Wahrheit zwei verschiedene Dinge testen wollen, die verschiedene Werkzeuge brauchen:
- Verhält sich mein Feature bei einem gegebenen Lizenzergebnis korrekt? (Ist der Export-Button in Free ausgeblendet? Erscheint der Seat-Hinweis am Limit?) Das ist der Großteil Ihrer Tests, und er sollte rein, schnell und frei von jeder Kryptografie sein.
- Löst das SDK eine gegebene Lizenz in das erwartete Ergebnis auf? (Schaltet dieser Pro-Schlüssel tatsächlich frei? Fällt ein manipulierter Schlüssel geschlossen aus?) Das ist eine Handvoll Tests, und sie sollten den echten Verifikationscode durchlaufen — nur ohne Server.
Schicht 1: eine Nahtstelle, die Sie mocken können
Das Keyright-.NET-SDK ist bewusst klein — Validate(), IsEnabled, GetLimit — und wirft nie eine Exception, was es bereits angenehm aufrufbar macht. Aber Ihre View-Models und Command-Handler sollten es nicht direkt aufrufen, denn dann zieht jeder Test davon den echten Client mit. Setzen Sie ein Interface mit einer Methode pro Frage davor:
public interface ILicenseGate
{
bool IsPaid { get; }
bool IsEnabled(string feature);
long GetLimit(string feature, long fallback);
}
Die Produktionsimplementierung ist ein dünner Wrapper um den einen KeyrightClient, den Sie beim Start erstellt haben:
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);
}
Ihr Feature-Code nimmt ILicenseGate, nie 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);
}
Jetzt ist das Fake trivial — und, was wichtig ist, es kann jeden Lizenzzustand sofort ausdrücken, auch die, die real nur mühsam herzustellen sind:
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;
}
Die Tests lesen sich wie Spezifikationen, laufen in Mikrosekunden und berühren kein Schlüsselmaterial:
[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);
}
Hierhin gehört die überwältigende Mehrheit Ihrer lizenzbezogenen Tests. Beachten Sie, dass FakeLicenseGate die Wire-Konvention von Keyright spiegelt — Entitlement-Werte sind Strings, "true"/"false" für Flags und eine Ganzzahl (oder "unlimited") für Limits —, sodass ein Fake, das "true" zurückgibt, genau das testet, was das echte Gate zurückgibt.
Schicht 2: echte Verifikation gegen eingecheckte Fixtures
Schicht 1 vertraut darauf, dass KeyrightLicenseGate das SDK korrekt abbildet. Eine zweite, deutlich kleinere Schicht beweist, dass das SDK selbst eine echte Lizenz in die erwartete Edition und die erwarteten Entitlements überführt — und das ganz ohne Server, denn Keyright ist offline-first: Validate() verifiziert eine Signatur gegen einen eingebetteten öffentlichen Schlüssel und braucht kein Netzwerk.
Der Trick: Lizenzschlüssel als Test-Fixtures behandeln. Legen Sie ein dediziertes Wegwerf-Testprodukt an (nennen Sie es acme-test, idealerweise in einem Nicht-Produktions-Workspace), stellen Sie einmalig ein paar Schlüssel aus — einen Pro-Schlüssel mit bekannten Entitlements, einen Enterprise-Schlüssel und einen Schlüssel für ein anderes Produkt — und checken Sie die Schlüssel-Strings zusammen mit dem öffentlichen Schlüssel dieses Testprodukts ein. Nichts davon ist sensibel: Der öffentliche Schlüssel wird ohnehin in jedem Binary ausgeliefert und kann nur verifizieren, und die Fixtures sind signierte Claims ohne Bezug zu Ihrem Produktionsschlüssel oder einem echten Kunden.
Übergeben Sie ein Fixture direkt über die Quelle LicenseString an einen echten Client — die Quelle mit der höchsten Priorität, sodass sie ohne Datei und ohne Cache gewinnt:
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();
}
Jetzt steht der echte Verifikationspfad unter Test, und jeder Fall ist deterministisch:
[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);
}
Der Test mit dem manipulierten Schlüssel ist derjenige, der die ganze Schicht rechtfertigt: Er beweist, dass Ihr Build geschlossen ausfällt. Sollte ein künftiges SDK-Update oder eine falsch gesetzte Option jemals dazu führen, dass eine ungültige Signatur zu einer bezahlten Edition aufgelöst wird, wird dieser Test in CI rot, lange bevor es ausgeliefert wird. Die Verdrahtung der Quellenpriorität testen Sie auf dieselbe Weise — legen Sie ein Fixture auf die Platte und setzen Sie LicenseFilePath, oder zeigen Sie mit der Umgebungsvariable KEYRIGHT_LICENSE darauf, um zu beweisen, dass Ihr Bootstrap die Lizenz von dort liest, wo Sie es annehmen.
Was bewusst aus der schnellen Suite herausbleibt
Drei Verhaltensweisen sind real, wichtig und falsch in einen Unit-Test zu zwingen: Ablauf, Widerruf und die Erkennung von Uhrmanipulation. Sie sind zeit- und netzwerkabhängig — ein tatsächlich abgelaufenes Lease, ein Widerruf, der sich beim nächsten Refresh ausbreitet, eine über die Toleranz hinaus zurückgedrehte Uhr — und sie auf SDK-Ebene überzeugend zu faken hieße, Serverzustand zu reproduzieren, den Sie nicht mocken sollten.
Testen Sie sie auf den beiden Ebenen, die passen. In Schicht 1 asserten Sie, wie Ihre App reagiert, wenn das Gate diese Zustände meldet (injizieren Sie ein Fake, dessen IsPaid false ist und dessen Status Expired oder Revoked lautet, und prüfen Sie, dass Sie den richtigen „Verlängern“-Hinweis zeigen). Und verifizieren Sie die echten Übergänge einmal, von Hand oder in einem skriptgesteuerten Job, gegen Ihren Test-Tenant — die genaue Abfolge ist die End-to-End-Checkliste im .NET-SDK-Leitfaden: aktivieren, das Netzwerk trennen und bestätigen, dass das gecachte Lease weiterhin validiert, dann widerrufen und bestätigen, dass der nächste Refresh auf Free zurückfällt. Das gehört in einen nächtlichen oder Pre-Release-Job, nicht in die Suite, die jeden Commit absichert.
Der Gewinn
Die Aufteilung hält jeden Test ehrlich darüber, was er wirklich prüft. Schicht 1 macht Ihre lizenzierten Codepfade so testbar wie jede andere Verzweigung — schnell, deterministisch und frei von Schlüsselmaterial —, sodass „in Free ausgeblendet, in Pro freigeschaltet“ nichts mehr ist, was Sie von Hand prüfen. Schicht 2 nagelt das eine fest, was Schicht 1 voraussetzt: dass das SDK eine echte signierte Lizenz auf das richtige Ergebnis abbildet und bei einer ungültigen geschlossen ausfällt — und das mit nichts als eingecheckten Fixtures und dem Offline-Verifizierer. Verdrahten Sie das Gate beim Start wie im .NET-SDK-Integrationsleitfaden, lesen Sie auf der Seite Sicherheit & Leases, wie sich Leases und Offline-Validierung verhalten, und der kostenlose Plan reicht mehr als aus, um die Handvoll Fixtures zu erzeugen, auf denen diese Strategie läuft.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.