Pruebas sin conexión con el kit de pruebas

Cada SDK de servidor (Go, TypeScript y Python) tiene un módulo testing para las pruebas unitarias y CI. Genera tokens de acceso y tokens de ID firmados con una clave de prueba fija, construye un verificador de tokens que acepta exclusivamente esos tokens sin ningún acceso a la red, le proporciona ejemplos de cargas útiles de eventos y firma las entregas de webhooks con un secreto de prueba público. Sus pruebas se ejecutan sin un entorno de KeenAuth, sin credenciales y sin acceso a Internet.

Cómo funciona

El modo de prueba no debilita las demás comprobaciones: los tokens caducados, una audiencia incorrecta, una carga útil modificada, un jti ausente y un token vinculado a DPoP sin prueba se rechazan exactamente igual que en producción. Consulte Verificación de tokens en su API para ver la lista completa.

Mantenga el modo de prueba solo en el código de pruebas. Cualquiera puede firmar tokens de prueba, por lo que un servicio que lo activara aceptaría tokens falsificados.

Go

El paquete es keenauthtest, al estilo de net/http/httptest.

import (
	"github.com/keenauth/keenauth-go/keenauthtest"
	"github.com/keenauth/keenauth-go/verify"
)

func TestOrdersAPI(t *testing.T) {
	v, err := keenauthtest.NewVerifier(verify.Config{Audience: "https://api.acme.example"})
	if err != nil {
		t.Fatal(err)
	}
	token, err := keenauthtest.AccessToken("https://api.acme.example",
		keenauthtest.WithClaims(map[string]any{"scope": "orders:read", "org_id": "org_01m3vnbwg0fvav6zw6w7v3st0r"}))
	if err != nil {
		t.Fatal(err)
	}
	// Pass v to the handler under test and send "Authorization: Bearer " + token.
	claims, err := v.VerifyAccessToken(context.Background(), token, nil)
	if err != nil || claims.Scope != "orders:read" {
		t.Fatal(err)
	}
}

keenauthtest.IDToken(clientID, ...) genera un token de ID. WithLifetime cambia la duración (un valor negativo genera un token caducado) y WithIssuedAt establece la fecha de emisión. En WithClaims, un valor nil elimina una reclamación. Sin el paquete, puede establecer usted mismo verify.Config{TestingMode: true}.

TypeScript

El módulo es el subpaquete @keenauth/node/testing.

import { createTestVerifier, mintAccessToken } from "@keenauth/node/testing";

const verifier = createTestVerifier({ audience: "https://api.acme.example" });
const token = mintAccessToken("https://api.acme.example", {
  claims: { scope: "orders:read", org_id: "org_01m3vnbwg0fvav6zw6w7v3st0r" },
});

const claims = await verifier.verifyAccessToken(token);
// claims.subject, claims.scope, claims.claims.org_id

mintIdToken(clientId, ...) genera un token de ID. Las opciones son claims (un valor null elimina una reclamación), lifetimeSeconds e issuedAt. Sin el módulo, puede pasar testing: true a createVerifier.

Python

El módulo es keenauth.testing; necesita el extra verify, como el verificador.

from keenauth import testing

verifier = testing.verifier(audience="https://api.acme.example")
token = testing.mint_access_token(
    "https://api.acme.example",
    claims={"scope": "orders:read", "org_id": "org_01m3vnbwg0fvav6zw6w7v3st0r"},
)

claims = verifier.verify_access_token(token)
# claims.subject, claims.scope, claims.claims["org_id"]

testing.mint_id_token(client_id, ...) genera un token de ID. Los argumentos con nombre son claims (un valor None elimina una reclamación), lifetime_seconds e issued_at. Sin el módulo, puede pasar testing=True a VerifierConfig.

Reclamaciones predeterminadas

Reclamación Token de acceso Token de ID
iss https://test.keenauth.invalid https://test.keenauth.invalid
sub usr_01m3vnbwg0e8mbe7h7xxp6rmkc usr_01m3vnbwg0e8mbe7h7xxp6rmkc
aud la audiencia que pase el ID de cliente que pase
client_id app_01m3vnbwg0fqba5ntanfd63s8n —
iat, exp ahora, ahora + 3600 segundos ahora, ahora + 3600 segundos
jti aleatorio —
auth_time — igual que iat

El encabezado del token de acceso tiene typ: at+jwt (RFC 9068). Añada las reclamaciones que lea su API, como scope, org_id, permissions, acr, amr o auth_time. Para los tokens de ID, añada nonce cuando su código lo compruebe.

Eventos de ejemplo

El kit incluye cargas útiles de ejemplo de eventos comunes: user.created, user.updated, user.deleted, session.created, session.revoked, organization.created y organization_membership.created. Usan el mismo sobre y los mismos esquemas que los eventos que envía KeenAuth, en el modo reducido predeterminado: identificadores, estado y nombres de los campos modificados, sin datos personales. Utilícelos para probar sus controladores de eventos.

Pruebas de webhooks

El kit firma una carga útil exactamente como una entrega de webhook de KeenAuth (Standard Webhooks v1): devuelve el cuerpo sin procesar y los encabezados webhook-id, webhook-timestamp y webhook-signature. Su controlador la verifica con el verificador de webhooks del SDK, el mismo código que se ejecuta en producción. Solo cambia el secreto.

Node

import { Webhook, WebhookVerificationError } from "@keenauth/node/webhooks";
import { sampleEvent, signWebhook, testWebhookSecret } from "@keenauth/node/testing";

const webhook = new Webhook(testWebhookSecret()); // in production: process.env.KEENAUTH_WEBHOOK_SECRET

const delivery = signWebhook(sampleEvent("user.created"));
const event = webhook.verify(delivery.body, delivery.headers); // the parsed event

// A delivery from an hour ago is refused.
const old = signWebhook(sampleEvent("user.created"), { timestamp: Math.floor(Date.now() / 1000) - 3600 });
try {
  webhook.verify(old.body, old.headers);
} catch (e) {
  // e instanceof WebhookVerificationError, e.kind === "timestamp"
}

signWebhook(payload, options) acepta una cadena, un Uint8Array o un objeto (serializado con JSON.stringify). Las opciones son secret, id y timestamp (segundos Unix o un Date). Para probar un controlador HTTP, envíe delivery.body como cuerpo sin procesar de la solicitud con delivery.headers y Content-Type: application/json.

Python

import time

from keenauth import testing
from keenauth.webhooks import WebhookTimestampError, WebhookVerifier

verifier = WebhookVerifier(testing.webhook_secret())  # in production: os.environ["KEENAUTH_WEBHOOK_SECRET"]

delivery = testing.sign_webhook(testing.sample_event("user.created"))
verifier.verify(delivery.body, delivery.headers)

# A delivery from an hour ago is refused.
old = testing.sign_webhook(testing.sample_event("user.created"), timestamp=int(time.time()) - 3600)
try:
    verifier.verify(old.body, old.headers)
except WebhookTimestampError:
    pass

testing.sign_webhook(payload, secret=None, msg_id=None, timestamp=None) acepta bytes, una cadena o un diccionario (serializado como JSON compacto) y devuelve delivery.body (bytes) y delivery.headers (un diccionario). Con un cliente de pruebas como el de Flask o el de FastAPI, envíe delivery.body como cuerpo sin procesar con delivery.headers.

Go

import (
	"net/http"
	"net/http/httptest"

	"github.com/keenauth/keenauth-go/keenauthtest"
	"github.com/keenauth/keenauth-go/webhook"
)

func TestWebhookHandler(t *testing.T) {
	v, err := webhook.NewVerifier([]string{keenauthtest.WebhookSecret()}) // in production: os.Getenv("KEENAUTH_WEBHOOK_SECRET")
	if err != nil {
		t.Fatal(err)
	}
	body, err := keenauthtest.Event("user.created")
	if err != nil {
		t.Fatal(err)
	}
	delivery, err := keenauthtest.SignWebhook(body)
	if err != nil {
		t.Fatal(err)
	}
	req, err := delivery.Request("http://localhost/webhooks/keenauth")
	if err != nil {
		t.Fatal(err)
	}
	rec := httptest.NewRecorder()
	newWebhookHandler(v).ServeHTTP(rec, req) // your handler, which calls v.Verify(r.Header, body)
	if rec.Code != http.StatusOK {
		t.Fatalf("status %d", rec.Code)
	}
}

keenauthtest.SignWebhook(payload, ...) devuelve delivery.Body y delivery.Header; delivery.Request(url) construye la solicitud POST con Content-Type: application/json. Las opciones son WithWebhookSecret, WithWebhookID y WithWebhookTimestamp.

Lo que el kit no hace