Skip to content
← Todas las publicaciones
· Delta1 Labs Licenciamiento.NETGuía

Probar el código que vive detrás de una comprobación de licencia

Tus funciones de pago están detrás de una puerta de licencia, pero ¿cómo las pruebas sin un servicio emisor en vivo, una clave real o el reloj del sistema? Aquí tienes una estrategia en dos capas para Keyright: una costura fina que simulas para pruebas unitarias rápidas, y un puñado de fixtures firmados con una clave de prueba y versionados en el repositorio que ejercitan la ruta real de verificación del SDK, totalmente sin conexión y de forma determinista en CI.

En algún lugar de tu app hay una rama que dice, en esencia, “si este cliente tiene licencia, haz lo valioso”. Esa rama es la razón de ser de distribuir un producto con licencia, y también es la rama que tus pruebas suelen saltarse, porque ejercitarla parece exigir un servidor de licencias, una clave real y, para los casos de caducidad, una máquina del tiempo. Así que el botón de exportar, el aviso de límite de asientos, el panel solo para enterprise se quedan sin probar, y la primera vez que alguien recorre la ruta con licencia de extremo a extremo es delante de un cliente que paga.

No tiene por qué ser así. Este artículo expone una estrategia en dos capas para probar código protegido por Keyright: una costura fina que simulas para la mayoría rápida de tus pruebas, y unos pocos fixtures firmados con una clave de prueba y versionados en el repositorio que recorren la ruta de verificación real del SDK, totalmente sin conexión, sin ningún servicio en el bucle y sin depender del reloj.

Por qué los enfoques ingenuos duelen

Dos instintos acaban mal. El primero es dejar que las pruebas hablen con tu servicio emisor real: ahora tu suite unitaria necesita red, una clave válida y un asiento que consume en cada ejecución, y se pone en rojo cada vez que el servicio está caído. El segundo es no probar el código protegido en absoluto y “verificarlo a mano”, lo que significa nunca.

La solución es darse cuenta de que en realidad hay dos cosas distintas que quieres probar, y quieren herramientas distintas:

  • ¿Se comporta correctamente mi función dado un resultado de licenciamiento? (¿Está oculto el botón de exportar en Free? ¿Salta el aviso de asientos al llegar al límite?) Esto es el grueso de tus pruebas, y debería ser puro, rápido y no tener nada que ver con la criptografía.
  • ¿Resuelve el SDK una licencia dada al resultado que espero? (¿Esta clave Pro realmente desbloquea? ¿Una clave manipulada falla cerrada?) Esto es un puñado de pruebas, y debería ejercitar el código real de verificación, solo que sin servidor.
Tu código de funcionesdepende de ILicenseGateILicenseGateCapa 1 — la mayoría de pruebasFakeLicenseGate (sin criptografía)inyecta cualquier edición / entitlementrápida · sin conexión · deterministaCapa 2 — unas pocas pruebasKeyrightClient realfixtures versionados con clave de pruebaverificación real · sin servidor

Capa 1: una costura que puedes simular

El SDK .NET de Keyright es deliberadamente pequeño —Validate(), IsEnabled, GetLimit— y nunca lanza excepciones, lo que ya lo hace agradable de invocar. Pero tus view-models y manejadores de comandos no deberían llamarlo directamente, porque entonces cada prueba de ellos arrastra el cliente real. Pon delante una interfaz con un método por pregunta:

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

La implementación de producción es un envoltorio fino sobre el único KeyrightClient que construiste al arrancar:

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);
}

El código de tus funciones recibe ILicenseGate, nunca 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);
}

Ahora el fake es trivial y, lo que es más importante, puede expresar cualquier estado de licenciamiento al instante, incluidos los que cuesta producir de verdad:

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;
}

Las pruebas se leen como especificaciones, se ejecutan en microsegundos y no tocan material de claves:

[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);
}

Aquí es donde pertenece la inmensa mayoría de tus pruebas relacionadas con el licenciamiento. Fíjate en que FakeLicenseGate refleja la propia convención de transporte de Keyright —los valores de entitlement son cadenas, "true"/"false" para los flags y un entero (o "unlimited") para los límites—, así que un fake que devuelve "true" prueba exactamente lo que devuelve la puerta real.

Capa 2: verificación real contra fixtures versionados

La capa 1 confía en que KeyrightLicenseGate mapea el SDK correctamente. Una segunda capa, mucho más pequeña, demuestra que el propio SDK convierte una licencia real en la edición y los entitlements que esperas, y puede hacerlo sin servidor, porque Keyright es offline-first: Validate() verifica una firma contra una clave pública embebida y no necesita red.

El truco está en tratar las claves de licencia como fixtures de prueba. Crea un producto de prueba desechable dedicado (llámalo acme-test, idealmente en un espacio de trabajo que no sea de producción), emite unas pocas claves una sola vez —una clave Pro con entitlements conocidos, una clave Enterprise y una clave de un producto distinto— y versiona las cadenas de clave junto con la clave pública de ese producto de prueba. Nada de esto es sensible: la clave pública ya se distribuye en cada binario y solo puede verificar, y los fixtures son claims firmados sin relación con tu clave de producción ni con ningún cliente real.

Pásale un fixture directamente a un cliente real a través de la fuente LicenseString —la fuente de mayor precedencia, así que gana sin que intervenga ningún archivo ni caché—:

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();
}

Ahora la ruta real de verificación está bajo prueba, y cada caso es determinista:

[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);
}

La prueba de la clave manipulada es la que justifica toda la capa: demuestra que tu build falla cerrado. Si una futura actualización del SDK o una opción mal configurada hiciera alguna vez que una firma inválida resolviera a una edición de pago, esta prueba se pondría en rojo en CI mucho antes de que se distribuyera. Puedes probar el cableado de precedencia de fuentes de la misma manera: deja un fixture en disco y configura LicenseFilePath, o apunta la variable de entorno KEYRIGHT_LICENSE a él, para demostrar que tu arranque lee la licencia de donde crees que la lee.

Lo que deliberadamente queda fuera de la suite rápida

Tres comportamientos son reales, importantes y erróneos de forzar dentro de una prueba unitaria: la caducidad, la revocación y la detección de manipulación del reloj. Dependen del tiempo y de la red —un lease caducado de verdad, una revocación que se propaga en el siguiente refresco, un reloj retrasado más allá de la tolerancia— y simularlos de forma convincente a nivel de SDK significa reproducir un estado del servidor que no deberías simular.

Pruébalos en los dos niveles que encajan. En la capa 1, verifica cómo reacciona tu app cuando la puerta informa de esos estados (inyecta un fake cuyo IsPaid sea false y cuyo estado sea Expired o Revoked, y comprueba que muestras el aviso de “renovar” correcto). Y verifica las transiciones reales una vez, a mano o en un trabajo con guion, contra tu tenant de prueba: la secuencia exacta es la lista de comprobación de extremo a extremo de la guía del SDK de .NET: activa, desconecta la red y confirma que el lease en caché sigue validando, luego revoca y confirma que el siguiente refresco baja a Free. Eso pertenece a un trabajo nocturno o previo al lanzamiento, no a la suite que bloquea cada commit.

La recompensa

La división mantiene a cada prueba honesta sobre lo que realmente comprueba. La capa 1 hace que tus rutas de código con licencia sean tan testeables como cualquier otra rama —rápidas, deterministas y libres de material de claves—, de modo que “oculto en Free, desbloqueado en Pro” deja de ser algo que verificas a mano. La capa 2 clava lo único que la capa 1 da por hecho, que el SDK mapea una licencia real firmada al resultado correcto y falla cerrado ante una inválida, y lo hace con nada más que fixtures versionados y el verificador sin conexión. Cablea la puerta al arrancar como en la guía de integración del SDK de .NET, lee cómo se comportan los leases y la validación sin conexión en la página de seguridad y leases, y el plan gratuito es más que suficiente para emitir el puñado de fixtures con los que funciona esta estrategia.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.