Skip to main content

Обзор

Тестирование L402 API включает два уровня:
  1. Юнит / интеграционные тесты — проверка логики middleware без реальных Lightning-платежей
  2. Сквозные тесты — проверка полного потока с реальным кошельком и реальными sats

Юнит-тесты — заглушка провайдера

Передайте заглушку LightningProvider, чтобы полностью обойти Lightning:
import { l402, LightningProvider, Invoice } from "l402-kit";
import request from "supertest";
import express from "express";

const mockProvider: LightningProvider = {
  async createInvoice(amountSats: number): Promise<Invoice> {
    const hash = "abc123def456abc123def456abc123def456abc123def456abc123def456abc1";
    const macaroon = Buffer.from(
      JSON.stringify({ hash, exp: Date.now() + 3_600_000 })
    ).toString("base64");
    return { paymentRequest: "lnbc1...", paymentHash: hash, macaroon, amountSats };
  },
  async checkPayment(paymentHash: string): Promise<boolean> {
    return true; // always paid in tests
  },
};

const app = express();
app.get("/premium", l402({ priceSats: 10, lightning: mockProvider }), (_req, res) => {
  res.json({ data: "ok" });
});

// Test 1 — no auth → 402
const res402 = await request(app).get("/premium");
assert(res402.status === 402);
assert(res402.body.invoice === "lnbc1...");

// Test 2 — valid token → 200
// SHA256("correct-preimage") must equal the paymentHash above, or use a real hash pair
const macaroon = res402.body.macaroon;
const preimage = "correct-preimage-hex"; // must satisfy SHA256(preimage) == paymentHash
const res200 = await request(app)
  .get("/premium")
  .set("Authorization", `L402 ${macaroon}:${preimage}`);
assert(res200.status === 200);

Генерация валидного preimage для тестов

import { createHash, randomBytes } from "crypto";

// Generate a real hash/preimage pair
const preimage = randomBytes(32).toString("hex");
const paymentHash = createHash("sha256").update(Buffer.from(preimage, "hex")).digest("hex");

// Use these in your mock provider
const mockProvider: LightningProvider = {
  async createInvoice(amountSats: number): Promise<Invoice> {
    const macaroon = Buffer.from(
      JSON.stringify({ hash: paymentHash, exp: Date.now() + 3_600_000 })
    ).toString("base64");
    return { paymentRequest: "lnbc1...", paymentHash, macaroon, amountSats };
  },
  async checkPayment(): Promise<boolean> { return true; },
};

// In your test:
// Authorization: L402 <macaroon>:<preimage>
// This satisfies SHA256(preimage) == paymentHash ✓

Python — pytest с заглушкой провайдера

import pytest
import hashlib
import secrets
import base64
import json
from fastapi.testclient import TestClient
from l402kit.types import LightningProvider, Invoice
from your_app import app

class MockProvider(LightningProvider):
    def __init__(self):
        self.preimage = secrets.token_hex(32)
        self.payment_hash = hashlib.sha256(bytes.fromhex(self.preimage)).hexdigest()

    async def create_invoice(self, amount_sats: int) -> Invoice:
        exp = int((__import__("time").time() + 3600) * 1000)
        macaroon = base64.b64encode(
            json.dumps({"hash": self.payment_hash, "exp": exp}).encode()
        ).decode()
        return Invoice(
            payment_request="lnbc1...",
            payment_hash=self.payment_hash,
            macaroon=macaroon,
            amount_sats=amount_sats,
        )

    async def check_payment(self, payment_hash: str) -> bool:
        return True

def test_402_then_200(monkeypatch):
    provider = MockProvider()
    monkeypatch.setattr("your_app.lightning", provider)
    client = TestClient(app)

    # Step 1: no auth → 402
    res = client.get("/premium")
    assert res.status_code == 402
    macaroon = res.json()["macaroon"]

    # Step 2: valid L402 token → 200
    auth = f"L402 {macaroon}:{provider.preimage}"
    res = client.get("/premium", headers={"Authorization": auth})
    assert res.status_code == 200

Тестирование защиты от повторного использования

Убедитесь, что preimage не может быть использован повторно:
const res1 = await request(app)
  .get("/premium")
  .set("Authorization", `L402 ${macaroon}:${preimage}`);
assert(res1.status === 200); // first use — ok

const res2 = await request(app)
  .get("/premium")
  .set("Authorization", `L402 ${macaroon}:${preimage}`);
assert(res2.status === 401); // replay — rejected
assert(res2.body.error === "Token already used");

CI-пайплайн

Используйте заглушку провайдера в CI — Lightning-нода и API-ключи не нужны:
# .github/workflows/test.yml
- name: Run tests
  run: npm test
  env:
    NODE_ENV: test
    # No BLINK_API_KEY needed — mock provider is used in test env
Управляйте выбором провайдера в зависимости от окружения:
const lightning = process.env.NODE_ENV === "test"
  ? mockProvider
  : ManagedProvider.fromAddress(process.env.LIGHTNING_ADDRESS!);

Сквозное тестирование с реальными sats

Для полного тестирования платёжного потока (staging/перед запуском):
  1. Установите priceSats: 1 — стоимость ~$0.0008 за запуск теста
  2. Используйте OpenNode sandbox (testMode: true) для оплаты без реальных денег:
    const lightning = new OpenNodeProvider(process.env.OPENNODE_KEY!, true); // testMode
    
  3. Или используйте ваш Blink-кошелёк — платежи в 1 sat практически бесплатны

Автоматизированное E2E с тестовым кошельком

import { L402Client, BlinkWallet } from "l402-kit";

// Use a dedicated test wallet with a small budget
const wallet = new BlinkWallet(
  process.env.TEST_BLINK_API_KEY!,
  process.env.TEST_BLINK_WALLET_ID!,
);
const client = new L402Client({ wallet, budgetSats: 100 });

const res = await client.fetch("http://localhost:3000/premium");
assert(res.ok);
const data = await res.json();
assert(data.data !== undefined);

Чеклист перед запуском в продакшн

1

Юнит-тесты проходят с заглушкой провайдера

Поток 402 → оплата → 200 проверен. Защита от повторного использования проверена (второй запрос возвращает 401).
2

Протестировано истечение срока действия токена

Установите exp: Date.now() - 1 в вашем тестовом macaroon — убедитесь, что middleware возвращает 401.
3

Полное E2E с реальным платежом при priceSats: 1

Реальный кошелёк, реальный платёж, реальный ответ 200 OK. Используйте Wallet of Satoshi или Blink на вашем телефоне.
4

Защита от повторного использования настроена корректно для вашего деплоя

Один процесс: стандартный in-memory адаптер подойдёт. Несколько процессов (Kubernetes, PM2 cluster): используйте адаптер Supabase или Redis. См. Руководство по продакшну.