
# Offline testen mit dem Test-Kit

Jedes Server-SDK (Go, TypeScript und Python) enthält ein `testing`-Modul für Unit-Tests und CI. Es erzeugt Zugriffstoken und ID-Token, die mit einem festen Testschlüssel signiert sind, baut einen Token-Verifier, der genau diese Token ohne jeglichen Netzwerkzugriff akzeptiert, stellt Beispiel-Nutzdaten für Ereignisse bereit und signiert Webhook-Zustellungen mit einem öffentlichen Test-Secret. Ihre Tests laufen ohne KeenAuth-Umgebung, ohne Zugangsdaten und ohne Internetverbindung.

## So funktioniert es

- **Ein fester, öffentlicher Testschlüssel.** Test-Token werden mit ES256 und der Schlüssel-ID `keenauth-test-1` signiert. Der Schlüssel ist bewusst öffentlich: Sein privater Teil wird aus einer veröffentlichten Bezeichnung abgeleitet, und der öffentliche Schlüsselsatz ist in jedem SDK enthalten. Vertrauen Sie diesem Schlüssel an keiner anderen Stelle.
- **Ein Aussteller, der nicht existieren kann.** Test-Token tragen den Aussteller `https://test.keenauth.invalid`. Die Top-Level-Domain `.invalid` ist reserviert (RFC 2606) und wird nie aufgelöst, daher kann keine echte Umgebung diesen Aussteller haben.
- **Expliziter Testmodus.** Der SDK-Verifier akzeptiert diesen Aussteller nur, wenn der Testmodus im Code aktiviert ist. Ein Verifier, der auf die normale Weise konfiguriert ist, lehnt jeden Aussteller unter `.invalid` als Konfigurationsfehler ab. So kann ein Produktions-Verifier niemals ein Test-Token akzeptieren, selbst wenn sein Aussteller versehentlich auf den Test-Aussteller gesetzt wäre. Ein mit dem Testschlüssel signiertes Token, das jedoch Ihren echten Aussteller trägt, wird ebenfalls abgelehnt, denn Ihre Umgebung veröffentlicht andere Schlüssel.
- **Kein Netzwerkzugriff.** Im Testmodus entnimmt der Verifier den Testschlüssel dem Speicher und ruft nichts ab. Er akzeptiert nur den Test-Aussteller.

Der Testmodus schwächt die übrigen Prüfungen nicht ab: Abgelaufene Token, eine falsche Audience, eine veränderte Nutzlast, ein fehlendes `jti` und ein DPoP-gebundenes Token ohne Nachweis werden genauso abgelehnt wie in der Produktion. Die vollständige Liste finden Sie unter [Token in Ihrer API verifizieren](/docs/sdk/token-verification).

Verwenden Sie den Testmodus nur im Testcode. Jeder kann Test-Token signieren, daher würde ein Dienst, der ihn aktiviert, gefälschte Token akzeptieren.

## Go

Das Paket heißt `keenauthtest` und folgt dem Stil von `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, ...)` erzeugt ein ID-Token. `WithLifetime` ändert die Gültigkeitsdauer (ein negativer Wert ergibt ein abgelaufenes Token), und `WithIssuedAt` legt den Ausstellungszeitpunkt fest. In `WithClaims` entfernt ein `nil`-Wert einen Claim. Ohne das Paket können Sie `verify.Config{TestingMode: true}` selbst setzen.

## TypeScript

Das Modul ist der Unterpfad `@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, ...)` erzeugt ein ID-Token. Die Optionen sind `claims` (ein `null`-Wert entfernt einen Claim), `lifetimeSeconds` und `issuedAt`. Ohne das Modul können Sie `testing: true` an `createVerifier` übergeben.

## Python

Das Modul heißt `keenauth.testing`. Wie der Verifier benötigt es das Extra `verify`.

```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, ...)` erzeugt ein ID-Token. Die Schlüsselwortargumente sind `claims` (ein `None`-Wert entfernt einen Claim), `lifetime_seconds` und `issued_at`. Ohne das Modul können Sie `testing=True` an `VerifierConfig` übergeben.

## Standard-Claims

| Claim | Zugriffstoken | ID-Token |
|---|---|---|
| `iss` | `https://test.keenauth.invalid` | `https://test.keenauth.invalid` |
| `sub` | `usr_01m3vnbwg0e8mbe7h7xxp6rmkc` | `usr_01m3vnbwg0e8mbe7h7xxp6rmkc` |
| `aud` | die von Ihnen übergebene Audience | die von Ihnen übergebene Client-ID |
| `client_id` | `app_01m3vnbwg0fqba5ntanfd63s8n` | — |
| `iat`, `exp` | jetzt, jetzt + 3600 Sekunden | jetzt, jetzt + 3600 Sekunden |
| `jti` | zufällig | — |
| `auth_time` | — | wie `iat` |

Der Header des Zugriffstokens enthält `typ: at+jwt` (RFC 9068). Fügen Sie die Claims hinzu, die Ihre API liest, etwa `scope`, `org_id`, `permissions`, `acr`, `amr` oder `auth_time`. Bei ID-Token fügen Sie `nonce` hinzu, wenn Ihr Code es prüft.

## Beispielereignisse

Das Kit enthält Beispiel-Nutzdaten für gängige Ereignisse: `user.created`, `user.updated`, `user.deleted`, `session.created`, `session.revoked`, `organization.created` und `organization_membership.created`. Sie verwenden denselben Envelope und dieselben Schemas wie die Ereignisse, die KeenAuth sendet, im Standardmodus „thin“: Kennungen, Zustand und Namen geänderter Felder, keine personenbezogenen Daten. Verwenden Sie sie, um Ihre Ereignis-Handler zu testen.

- Go: `keenauthtest.Event("user.created")` gibt das JSON zurück; `keenauthtest.EventTypes()` listet die Typen auf.
- TypeScript: `sampleEvent("user.created")` und `sampleEventTypes()`.
- Python: `testing.sample_event("user.created")` und `testing.event_types()`.

## Webhooks testen

Das Kit signiert die Nutzdaten genau wie eine KeenAuth-Webhook-Zustellung (Standard Webhooks v1). Es gibt den rohen Body sowie die Header `webhook-id`, `webhook-timestamp` und `webhook-signature` zurück. Ihr Handler verifiziert sie mit dem Webhook-Verifier des SDK, demselben Code, der in der Produktion läuft. Nur das Secret unterscheidet sich.

- **Ein öffentliches Test-Secret.** Standardmäßig signiert das Kit mit einem festen `idp_whsec_`-Test-Secret, dessen Schlüssel aus der veröffentlichten Bezeichnung `KeenAuth offline test kit webhook secret (public, TEST ONLY)` abgeleitet wird. Jeder kann es berechnen, konfigurieren Sie es daher niemals an einem echten Endpunkt. Konfigurieren Sie Ihren Verifier in Tests damit oder übergeben Sie dem Helfer Ihr eigenes Secret.
- **Wie bei einer echten Zustellung.** `webhook-id` ist die `id` des Ereignisses (oder eine zufällige `msg_`-ID, wenn die Nutzdaten keine haben), und `webhook-timestamp` ist der aktuelle Zeitpunkt, sofern Sie keine Werte setzen. Der Body wird genau so gesendet, wie Sie ihn übergeben.
- **Wiederholung und Manipulation.** Signieren Sie mit einem alten Zeitstempel, um zu testen, ob Ihr Handler Zustellungen außerhalb des 5-Minuten-Fensters ablehnt, und verändern Sie den Body nach dem Signieren, um zu testen, ob er eine ungültige Signatur ablehnt.

### 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)` nimmt einen String, ein `Uint8Array` oder ein Objekt entgegen (serialisiert mit `JSON.stringify`). Die Optionen sind `secret`, `id` und `timestamp` (Unix-Sekunden oder ein `Date`). Um einen HTTP-Handler zu testen, senden Sie `delivery.body` als rohen Request-Body mit den Headern `delivery.headers` und `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)` nimmt Bytes, einen String oder ein Dictionary entgegen (serialisiert als kompaktes JSON) und gibt `delivery.body` (Bytes) und `delivery.headers` (ein Dictionary) zurück. Mit einem Test-Client wie dem von Flask oder FastAPI senden Sie `delivery.body` als rohen Body mit `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, ...)` gibt `delivery.Body` und `delivery.Header` zurück; `delivery.Request(url)` erstellt die POST-Anfrage mit `Content-Type: application/json`. Die Optionen sind `WithWebhookSecret`, `WithWebhookID` und `WithWebhookTimestamp`.

## Was das Kit nicht leistet

- Es emuliert KeenAuth nicht. Anmeldeabläufe, Regeln für die Token-Ausstellung, Ratenbegrenzungen und jedes andere serverseitige Verhalten sind nur in Ihrer gehosteten Umgebung `development` echt. Führen Sie Ihre Integrationstests dort aus, bevor Sie ausliefern.
- Es gibt keine Test-Uhr. Um die Zeit zu steuern, übergeben Sie dem Verifier Ihre eigene Uhr (`Now`, `now`) und legen den Ausstellungszeitpunkt des Tokens fest.
- Es erstellt keine DPoP-Nachweise. Sie können einen Claim `cnf.jkt` hinzufügen, müssen den Nachweis dann aber selbst signieren.
- Es liefert keine Webhooks aus. Echte Zustellungen, Wiederholungen und die Auswahl der Ereignisse sind nur in Ihrer gehosteten Umgebung `development` echt.
