
# Test offline con il test kit

Ogni SDK server (Go, TypeScript e Python) ha un modulo `testing` per unit test e CI. Emette access token e ID token firmati con una chiave di test fissa, costruisce un verificatore di token che accetta esattamente quei token senza alcun accesso alla rete, fornisce payload di evento di esempio e firma le consegne dei webhook con un segreto di test pubblico. I tuoi test girano senza un ambiente KeenAuth, senza credenziali e senza internet.

## Come funziona

- **Una chiave di test fissa e pubblica.** I token di test sono firmati con ES256 e con l'ID di chiave `keenauth-test-1`. La chiave è volutamente pubblica: la sua parte privata deriva da un'etichetta pubblicata, e il set di chiavi pubbliche è incluso in ogni SDK. Non fidarti mai di questa chiave in nessun altro contesto.
- **Un issuer che non può esistere.** I token di test portano l'issuer `https://test.keenauth.invalid`. Il dominio di primo livello `.invalid` è riservato (RFC 2606) e non si risolve mai, quindi nessun ambiente reale può avere questo issuer.
- **Modalità di test esplicita.** Il verificatore dell'SDK accetta questo issuer solo quando la modalità di test è attivata nel codice. Un verificatore configurato in modo normale rifiuta qualsiasi issuer sotto `.invalid` come errore di configurazione, quindi un verificatore di produzione non potrà mai accettare un token di test, anche se il suo issuer fosse stato impostato per errore sull'issuer di test. Anche un token firmato con la chiave di test ma con il tuo issuer reale viene rifiutato, perché il tuo ambiente pubblica chiavi diverse.
- **Nessuna rete.** In modalità di test il verificatore prende la chiave di test dalla memoria e non scarica mai nulla. Accetta solo l'issuer di test.

La modalità di test non indebolisce gli altri controlli: i token scaduti, un audience errato, un payload modificato, un `jti` mancante e un token vincolato con DPoP senza una prova vengono rifiutati esattamente come in produzione. Per l'elenco completo vedi [Verifica dei token nella tua API](/docs/sdk/token-verification).

Tieni la modalità di test solo nel codice di test. Chiunque può firmare token di test, quindi un servizio che l'abilitasse accetterebbe token contraffatti.

## Go

Il pacchetto è `keenauthtest`, sullo stile di `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)
	}
	// Passa v all'handler sotto test e invia "Authorization: Bearer " + token.
	claims, err := v.VerifyAccessToken(context.Background(), token, nil)
	if err != nil || claims.Scope != "orders:read" {
		t.Fatal(err)
	}
}
```

`keenauthtest.IDToken(clientID, ...)` emette un ID token. `WithLifetime` cambia la durata (un valore negativo genera un token scaduto) e `WithIssuedAt` imposta la data di emissione. In `WithClaims` un valore `nil` rimuove un claim. Senza il pacchetto puoi impostare tu stesso `verify.Config{TestingMode: true}`.

## TypeScript

Il modulo è il sottopercorso `@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, ...)` emette un ID token. Le opzioni sono `claims` (un valore `null` rimuove un claim), `lifetimeSeconds` e `issuedAt`. Senza il modulo puoi passare `testing: true` a `createVerifier`.

## Python

Il modulo è `keenauth.testing`; richiede l'extra `verify`, come il verificatore.

```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, ...)` emette un ID token. Gli argomenti con nome sono `claims` (un valore `None` rimuove un claim), `lifetime_seconds` e `issued_at`. Senza il modulo puoi passare `testing=True` a `VerifierConfig`.

## Claim predefiniti

| Claim | Access token | ID token |
|---|---|---|
| `iss` | `https://test.keenauth.invalid` | `https://test.keenauth.invalid` |
| `sub` | `usr_01m3vnbwg0e8mbe7h7xxp6rmkc` | `usr_01m3vnbwg0e8mbe7h7xxp6rmkc` |
| `aud` | l'audience che passi | il client ID che passi |
| `client_id` | `app_01m3vnbwg0fqba5ntanfd63s8n` | — |
| `iat`, `exp` | ora, ora + 3600 secondi | ora, ora + 3600 secondi |
| `jti` | casuale | — |
| `auth_time` | — | uguale a `iat` |

L'header dell'access token ha `typ: at+jwt` (RFC 9068). Aggiungi i claim che la tua API legge, come `scope`, `org_id`, `permissions`, `acr`, `amr` o `auth_time`. Per gli ID token, aggiungi `nonce` quando il tuo codice lo controlla.

## Eventi di esempio

Il kit include payload di esempio dei principali eventi: `user.created`, `user.updated`, `user.deleted`, `session.created`, `session.revoked`, `organization.created` e `organization_membership.created`. Usano la stessa envelope e gli stessi schemi degli eventi che KeenAuth invia, nella modalità thin predefinita: identificatori, stato e nomi dei campi modificati, nessun dato personale. Usali per testare i tuoi gestori di eventi.

- Go: `keenauthtest.Event("user.created")` restituisce il JSON; `keenauthtest.EventTypes()` elenca i tipi.
- TypeScript: `sampleEvent("user.created")` e `sampleEventTypes()`.
- Python: `testing.sample_event("user.created")` e `testing.event_types()`.

## Test dei webhook

Il kit firma un payload esattamente come una consegna di webhook di KeenAuth (Standard Webhooks v1): restituisce il body grezzo e gli header `webhook-id`, `webhook-timestamp` e `webhook-signature`. Il tuo handler lo verifica con il verificatore webhook dell'SDK, lo stesso codice che gira in produzione. Cambia solo il segreto.

- **Un segreto di test pubblico.** Per impostazione predefinita il kit firma con un segreto di test fisso `idp_whsec_` la cui chiave deriva dall'etichetta pubblicata `KeenAuth offline test kit webhook secret (public, TEST ONLY)`. Chiunque può calcolarlo, quindi non configurarlo mai su un endpoint reale. Configura il tuo verificatore con esso nei test, oppure passa un tuo segreto all'helper.
- **Uguale a una consegna reale.** `webhook-id` è l'`id` dell'evento (o un ID `msg_` casuale quando il payload non ne ha uno) e `webhook-timestamp` è l'ora attuale, salvo che tu li imposti. Il body viene inviato esattamente come lo passi.
- **Replay e manomissione.** Firma con un timestamp vecchio per verificare che il tuo handler rifiuti le consegne fuori dalla finestra di 5 minuti, e modifica il body dopo la firma per verificare che rifiuti una firma non valida.

### Node

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

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

const delivery = signWebhook(sampleEvent("user.created"));
const event = webhook.verify(delivery.body, delivery.headers); // l'evento analizzato

// Una consegna di un'ora fa viene rifiutata.
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)` accetta una stringa, un `Uint8Array` o un oggetto (serializzato con `JSON.stringify`). Le opzioni sono `secret`, `id` e `timestamp` (secondi Unix o un `Date`). Per testare un handler HTTP, invia `delivery.body` come body grezzo della richiesta con gli header di `delivery.headers` e `Content-Type: application/json`.

### Python

```python
import time

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

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

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

# Una consegna di un'ora fa viene rifiutata.
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)` accetta bytes, una stringa o un dizionario (serializzato come JSON compatto) e restituisce `delivery.body` (bytes) e `delivery.headers` (un dizionario). Con un client di test come quelli di Flask o FastAPI, invia `delivery.body` come body grezzo 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 produzione: 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) // il tuo handler, che chiama v.Verify(r.Header, body)
	if rec.Code != http.StatusOK {
		t.Fatalf("status %d", rec.Code)
	}
}
```

`keenauthtest.SignWebhook(payload, ...)` restituisce `delivery.Body` e `delivery.Header`; `delivery.Request(url)` costruisce la richiesta POST con `Content-Type: application/json`. Le opzioni sono `WithWebhookSecret`, `WithWebhookID` e `WithWebhookTimestamp`.

## Cosa non fa il kit

- Non emula KeenAuth. I flussi di accesso, le regole di emissione dei token, i rate limit e ogni altro comportamento del server sono reali solo nel tuo ambiente ospitato `development`; esegui lì i test di integrazione prima di rilasciare.
- Non esiste un orologio di test. Per controllare il tempo, passa al verificatore un tuo orologio (`Now`, `now`) e imposta la data di emissione del token.
- Non crea prove DPoP. Puoi aggiungere un claim `cnf.jkt`, ma in quel caso devi firmare la prova tu stesso.
- Non consegna webhook. Le consegne reali, i tentativi e la selezione degli eventi sono reali solo nel tuo ambiente ospitato `development`.
