e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
143 lines
4.4 KiB
Go
143 lines
4.4 KiB
Go
package x402
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
type mockFacilitator struct {
|
|
valid bool
|
|
reason string
|
|
}
|
|
|
|
func (m mockFacilitator) Verify(ctx context.Context, payment string, req Requirements) (Result, error) {
|
|
return Result{Valid: m.valid, Reason: m.reason, Payer: "0xpayer", Settlement: "0xtx"}, nil
|
|
}
|
|
|
|
func paidHandler(cfg Config) http.Handler {
|
|
served := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("ok"))
|
|
})
|
|
return Middleware(cfg)(served)
|
|
}
|
|
|
|
// No payment yields a 402 with the requirements describing where to pay.
|
|
func TestChallengeWhenNoPayment(t *testing.T) {
|
|
h := paidHandler(Config{PayTo: "0xabc", Network: "solana", Amount: "10000", Facilitator: mockFacilitator{valid: true}})
|
|
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/tool", nil))
|
|
|
|
if rec.Code != http.StatusPaymentRequired {
|
|
t.Fatalf("status = %d, want 402", rec.Code)
|
|
}
|
|
var ch challenge
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &ch); err != nil {
|
|
t.Fatalf("challenge body: %v", err)
|
|
}
|
|
if ch.X402Version != Version || len(ch.Accepts) != 1 {
|
|
t.Fatalf("unexpected challenge: %+v", ch)
|
|
}
|
|
req := ch.Accepts[0]
|
|
if req.PayTo != "0xabc" || req.Network != "solana" || req.MaxAmountRequired != "10000" {
|
|
t.Errorf("requirements not advertised correctly: %+v", req)
|
|
}
|
|
}
|
|
|
|
// A payment the facilitator accepts lets the request through, and the
|
|
// settlement is surfaced on the response.
|
|
func TestServesWhenPaymentValid(t *testing.T) {
|
|
h := paidHandler(Config{PayTo: "0xabc", Amount: "10000", Facilitator: mockFacilitator{valid: true}})
|
|
|
|
r := httptest.NewRequest(http.MethodGet, "/tool", nil)
|
|
r.Header.Set(PaymentHeader, "base64-payment-payload")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, r)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rec.Code)
|
|
}
|
|
if rec.Header().Get(PaymentResponseHeader) != "0xtx" {
|
|
t.Errorf("settlement not surfaced in %s", PaymentResponseHeader)
|
|
}
|
|
}
|
|
|
|
// A payment the facilitator rejects gets a 402 with the reason.
|
|
func TestChallengeWhenPaymentInvalid(t *testing.T) {
|
|
h := paidHandler(Config{PayTo: "0xabc", Amount: "10000", Facilitator: mockFacilitator{valid: false, reason: "insufficient amount"}})
|
|
|
|
r := httptest.NewRequest(http.MethodGet, "/tool", nil)
|
|
r.Header.Set(PaymentHeader, "bad-payment")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, r)
|
|
|
|
if rec.Code != http.StatusPaymentRequired {
|
|
t.Fatalf("status = %d, want 402", rec.Code)
|
|
}
|
|
var ch challenge
|
|
json.Unmarshal(rec.Body.Bytes(), &ch)
|
|
if ch.Error != "insufficient amount" {
|
|
t.Errorf("reason not surfaced: %q", ch.Error)
|
|
}
|
|
}
|
|
|
|
// A free amount ("" or "0") requires no payment.
|
|
func TestRequireFreeAmount(t *testing.T) {
|
|
cfg := Config{PayTo: "0xabc", Facilitator: mockFacilitator{valid: false}}
|
|
rec := httptest.NewRecorder()
|
|
if !cfg.Require(rec, httptest.NewRequest(http.MethodGet, "/free", nil), "0", "free.tool") {
|
|
t.Error("a zero amount should be free and proceed")
|
|
}
|
|
// Require doesn't write on success; recorder defaults to 200.
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
}
|
|
|
|
// Per-tool amounts override the default.
|
|
func TestAmountFor(t *testing.T) {
|
|
cfg := Config{Amount: "100", Amounts: map[string]string{"paid.Tool.Do": "5000"}}
|
|
if got := cfg.AmountFor("paid.Tool.Do"); got != "5000" {
|
|
t.Errorf("per-tool amount = %q, want 5000", got)
|
|
}
|
|
if got := cfg.AmountFor("other.Tool.Do"); got != "100" {
|
|
t.Errorf("default amount = %q, want 100", got)
|
|
}
|
|
}
|
|
|
|
// LoadConfig reads an operator x402 config file.
|
|
func TestLoadConfig(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "x402.json")
|
|
os.WriteFile(path, []byte(`{
|
|
"payTo": "0xabc", "network": "solana", "asset": "USDC",
|
|
"amount": "0", "amounts": {"weather.Weather.Forecast": "10000"}
|
|
}`), 0o644)
|
|
|
|
cfg, err := LoadConfig(path)
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig: %v", err)
|
|
}
|
|
if cfg.PayTo != "0xabc" || cfg.Network != "solana" {
|
|
t.Errorf("config not parsed: %+v", cfg)
|
|
}
|
|
if cfg.AmountFor("weather.Weather.Forecast") != "10000" {
|
|
t.Errorf("per-tool amount not loaded: %+v", cfg.Amounts)
|
|
}
|
|
if cfg.AmountFor("anything.else") != "0" {
|
|
t.Errorf("default amount = %q, want 0", cfg.AmountFor("anything.else"))
|
|
}
|
|
}
|
|
|
|
// Network defaults to base when unset.
|
|
func TestNetworkDefault(t *testing.T) {
|
|
if got := (Config{}).network(); got != "base" {
|
|
t.Errorf("default network = %q, want base", got)
|
|
}
|
|
}
|