
# 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

- **Una clave de prueba fija y pública.** Los tokens de prueba se firman con ES256 y con el ID de clave `keenauth-test-1`. La clave es deliberadamente pública: su parte privada se deriva de una etiqueta publicada, y el conjunto de claves públicas está incluido en cada SDK. No confíe nunca en esta clave en ningún otro contexto.
- **Un emisor que no puede existir.** Los tokens de prueba llevan el emisor `https://test.keenauth.invalid`. El dominio de nivel superior `.invalid` está reservado (RFC 2606) y nunca se resuelve, por lo que ningún entorno real puede tener este emisor.
- **Modo de prueba explícito.** El verificador del SDK acepta este emisor solo cuando el modo de prueba se activa en el código. Un verificador configurado de la forma habitual rechaza cualquier emisor bajo `.invalid` como error de configuración, de modo que un verificador de producción nunca puede aceptar un token de prueba, aunque su emisor se hubiera configurado por error como el emisor de prueba. Un token firmado con la clave de prueba, pero que lleve su emisor real, también se rechaza, porque su entorno publica claves distintas.
- **Sin red.** En modo de prueba, el verificador toma la clave de prueba de la memoria y nunca descarga nada. Solo acepta el emisor de prueba.

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](/docs/sdk/token-verification) 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`.

```go
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`.

```ts
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.

```python
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.

- Go: `keenauthtest.Event("user.created")` devuelve el JSON; `keenauthtest.EventTypes()` enumera los tipos.
- TypeScript: `sampleEvent("user.created")` y `sampleEventTypes()`.
- Python: `testing.sample_event("user.created")` y `testing.event_types()`.

## 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.

- **Un secreto de prueba público.** De forma predeterminada, el kit firma con un secreto de prueba fijo `idp_whsec_` cuya clave se deriva de la etiqueta publicada `KeenAuth offline test kit webhook secret (public, TEST ONLY)`. Cualquiera puede calcularlo, por lo que nunca debe configurarlo en un endpoint real. Configure su verificador con él en las pruebas o pase su propio secreto al asistente.
- **Igual que una entrega real.** `webhook-id` es el `id` del evento (o un ID `msg_` aleatorio cuando la carga útil no tiene uno) y `webhook-timestamp` es el momento actual, salvo que lo establezca. El cuerpo se envía exactamente como lo pase.
- **Repetición y manipulación.** Firme con una marca de tiempo antigua para comprobar que su controlador rechaza las entregas fuera de la ventana de 5 minutos, y modifique el cuerpo después de firmarlo para comprobar que rechaza una firma incorrecta.

### Node

```ts
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

```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

```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

- No emula KeenAuth. Los flujos de inicio de sesión, las reglas de emisión de tokens, los límites de frecuencia y cualquier otro comportamiento del servidor solo son reales en su entorno alojado `development`; ejecute sus pruebas de integración allí antes de publicar.
- No hay un reloj de pruebas. Para controlar el tiempo, pase su propio reloj al verificador (`Now`, `now`) y establezca la fecha de emisión del token.
- No genera pruebas DPoP. Puede añadir una reclamación `cnf.jkt`, pero entonces debe firmar la prueba usted mismo.
- No entrega webhooks. Las entregas reales, los reintentos y la selección de eventos solo son reales en su entorno alojado `development`.
