
# Tester hors ligne avec le kit de test

Chaque SDK serveur (Go, TypeScript et Python) dispose d'un module `testing` pour les tests unitaires et la CI. Il génère des jetons d'accès et des jetons ID signés avec une clé de test fixe, crée un vérificateur de jetons qui n'accepte que ces jetons sans aucun accès réseau, fournit des exemples de charges utiles d'événements et signe les livraisons de webhooks avec un secret de test public. Vos tests s'exécutent sans environnement KeenAuth, sans identifiants et sans accès à Internet.

## Fonctionnement

- **Une clé de test fixe et publique.** Les jetons de test sont signés avec ES256 et l'identifiant de clé `keenauth-test-1`. La clé est volontairement publique : sa partie privée est dérivée d'un libellé publié, et l'ensemble des clés publiques figure dans chaque SDK. Ne faites jamais confiance à cette clé ailleurs.
- **Un émetteur qui ne peut pas exister.** Les jetons de test portent l'émetteur `https://test.keenauth.invalid`. Le domaine de premier niveau `.invalid` est réservé (RFC 2606) et ne se résout jamais ; aucun environnement réel ne peut donc avoir cet émetteur.
- **Un mode de test explicite.** Le vérificateur du SDK n'accepte cet émetteur que si le mode de test est activé dans le code. Un vérificateur configuré de la manière habituelle refuse tout émetteur sous `.invalid` comme erreur de configuration : un vérificateur de production ne peut donc jamais accepter un jeton de test, même si son émetteur a été réglé par erreur sur l'émetteur de test. Un jeton signé avec la clé de test mais portant votre émetteur réel est également rejeté, car votre environnement publie d'autres clés.
- **Aucun accès réseau.** En mode de test, le vérificateur charge la clé de test depuis la mémoire et ne récupère jamais rien. Il n'accepte que l'émetteur de test.

Le mode de test n'affaiblit pas les autres contrôles : les jetons expirés, une audience incorrecte, une charge utile modifiée, un `jti` manquant et un jeton lié à DPoP sans preuve sont rejetés exactement comme en production. Consultez [Vérifier les jetons dans votre API](/docs/sdk/token-verification) pour la liste complète.

Réservez le mode de test au code de test. N'importe qui peut signer des jetons de test : un service qui l'activerait accepterait donc des jetons falsifiés.

## Go

Le package est `keenauthtest`, sur le modèle 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)
	}
	// Passez v au gestionnaire testé et envoyez "Authorization: Bearer " + token.
	claims, err := v.VerifyAccessToken(context.Background(), token, nil)
	if err != nil || claims.Scope != "orders:read" {
		t.Fatal(err)
	}
}
```

`keenauthtest.IDToken(clientID, ...)` génère un jeton ID. `WithLifetime` modifie la durée de vie (une valeur négative donne un jeton expiré) et `WithIssuedAt` définit la date d'émission. Dans `WithClaims`, une valeur `nil` supprime une revendication. Sans le package, vous pouvez définir vous-même `verify.Config{TestingMode: true}`.

## TypeScript

Le module est le sous-chemin `@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, ...)` génère un jeton ID. Les options sont `claims` (une valeur `null` supprime une revendication), `lifetimeSeconds` et `issuedAt`. Sans le module, vous pouvez passer `testing: true` à `createVerifier`.

## Python

Le module est `keenauth.testing` ; il nécessite l'extra `verify`, comme le vérificateur.

```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, ...)` génère un jeton ID. Les arguments nommés sont `claims` (une valeur `None` supprime une revendication), `lifetime_seconds` et `issued_at`. Sans le module, vous pouvez passer `testing=True` à `VerifierConfig`.

## Revendications par défaut

| Revendication | Jeton d'accès | Jeton ID |
|---|---|---|
| `iss` | `https://test.keenauth.invalid` | `https://test.keenauth.invalid` |
| `sub` | `usr_01m3vnbwg0e8mbe7h7xxp6rmkc` | `usr_01m3vnbwg0e8mbe7h7xxp6rmkc` |
| `aud` | l'audience que vous passez | l'identifiant client que vous passez |
| `client_id` | `app_01m3vnbwg0fqba5ntanfd63s8n` | — |
| `iat`, `exp` | maintenant, maintenant + 3600 secondes | maintenant, maintenant + 3600 secondes |
| `jti` | aléatoire | — |
| `auth_time` | — | identique à `iat` |

L'en-tête du jeton d'accès contient `typ: at+jwt` (RFC 9068). Ajoutez les revendications que lit votre API, comme `scope`, `org_id`, `permissions`, `acr`, `amr` ou `auth_time`. Pour les jetons ID, ajoutez `nonce` si votre code le vérifie.

## Exemples d'événements

Le kit comprend des exemples de charges utiles pour les événements courants : `user.created`, `user.updated`, `user.deleted`, `session.created`, `session.revoked`, `organization.created` et `organization_membership.created`. Ils utilisent la même enveloppe et les mêmes schémas que les événements envoyés par KeenAuth, en mode allégé par défaut : identifiants, état et noms des champs modifiés, sans données personnelles. Utilisez-les pour tester vos gestionnaires d'événements.

- Go : `keenauthtest.Event("user.created")` renvoie le JSON ; `keenauthtest.EventTypes()` liste les types.
- TypeScript : `sampleEvent("user.created")` et `sampleEventTypes()`.
- Python : `testing.sample_event("user.created")` et `testing.event_types()`.

## Tester les webhooks

Le kit signe une charge utile exactement comme une livraison de webhook KeenAuth (Standard Webhooks v1) : il renvoie le corps brut ainsi que les en-têtes `webhook-id`, `webhook-timestamp` et `webhook-signature`. Votre gestionnaire la vérifie avec le vérificateur de webhooks du SDK, c'est-à-dire le même code qu'en production. Seul le secret diffère.

- **Un secret de test public.** Par défaut, le kit signe avec un secret de test `idp_whsec_` fixe, dont la clé est dérivée du libellé publié `KeenAuth offline test kit webhook secret (public, TEST ONLY)`. N'importe qui peut le calculer : ne le configurez jamais sur un point de terminaison réel. Configurez votre vérificateur avec ce secret dans vos tests, ou transmettez votre propre secret à l'assistant.
- **Identique à une livraison réelle.** `webhook-id` correspond à l'`id` de l'événement (ou à un identifiant `msg_` aléatoire si la charge utile n'en a pas) et `webhook-timestamp` correspond à l'instant présent, sauf si vous les définissez. Le corps est envoyé exactement tel que vous le passez.
- **Rejeu et altération.** Signez avec un horodatage ancien pour vérifier que votre gestionnaire refuse les livraisons hors de la fenêtre de 5 minutes, et modifiez le corps après la signature pour vérifier qu'il refuse une signature invalide.

### Node

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

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

const delivery = signWebhook(sampleEvent("user.created"));
const event = webhook.verify(delivery.body, delivery.headers); // l'événement analysé

// Une livraison datant d'une heure est refusée.
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)` accepte une chaîne, un `Uint8Array` ou un objet (sérialisé avec `JSON.stringify`). Les options sont `secret`, `id` et `timestamp` (secondes Unix ou un `Date`). Pour tester un gestionnaire HTTP, envoyez `delivery.body` comme corps brut de la requête avec `delivery.headers` et `Content-Type: application/json`.

### Python

```python
import time

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

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

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

# Une livraison datant d'une heure est refusée.
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)` accepte des octets, une chaîne ou un dictionnaire (sérialisé en JSON compact) et renvoie `delivery.body` (octets) et `delivery.headers` (un dictionnaire). Avec un client de test comme ceux de Flask ou FastAPI, envoyez `delivery.body` comme corps brut avec `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()}) // en 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) // votre gestionnaire, qui appelle v.Verify(r.Header, body)
	if rec.Code != http.StatusOK {
		t.Fatalf("status %d", rec.Code)
	}
}
```

`keenauthtest.SignWebhook(payload, ...)` renvoie `delivery.Body` et `delivery.Header` ; `delivery.Request(url)` construit la requête POST avec `Content-Type: application/json`. Les options sont `WithWebhookSecret`, `WithWebhookID` et `WithWebhookTimestamp`.

## Ce que le kit ne fait pas

- Il n'émule pas KeenAuth. Les flux de connexion, les règles d'émission des jetons, les limites de débit et tous les autres comportements du serveur ne sont réels que dans votre environnement hébergé `development` ; exécutez vos tests d'intégration dans cet environnement avant toute mise en production.
- Il n'existe pas d'horloge de test. Pour contrôler le temps, passez votre propre horloge au vérificateur (`Now`, `now`) et définissez la date d'émission du jeton.
- Il ne crée pas de preuves DPoP. Vous pouvez ajouter une revendication `cnf.jkt`, mais vous devez alors signer la preuve vous-même.
- Il ne livre pas de webhooks. Les livraisons réelles, les nouvelles tentatives et la sélection des événements ne sont réelles que dans votre environnement hébergé `development`.
