From 0fc0dd11178cc7dbfa16671718644cd5f6954159 Mon Sep 17 00:00:00 2001 From: Kirtiman Singh Date: Mon, 1 Jun 2026 19:50:25 +0530 Subject: [PATCH] Add integration tests for x402 premium endpoints --- package.json | 3 +- src/routes/premium-routes.js | 67 ++++++++++ src/server.js | 66 ++------- tests/premium-endpoints.integration.test.js | 140 ++++++++++++++++++++ 4 files changed, 219 insertions(+), 57 deletions(-) create mode 100644 src/routes/premium-routes.js create mode 100644 tests/premium-endpoints.integration.test.js diff --git a/package.json b/package.json index 187fef1..d6b1d1d 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,9 @@ "record:narrated": "node src/render-narrated-demo.js", "setup": "node src/setup-wallets.js", "setup:usdc": "node src/setup-usdc.js", - "test": "node src/agents/settlement-header.test.js", + "test": "node src/agents/settlement-header.test.js && node tests/premium-endpoints.integration.test.js", "test:parser": "node src/agents/settlement-header.test.js", + "test:premium": "node tests/premium-endpoints.integration.test.js", "test:demo": "node src/demo.js" }, "keywords": [ diff --git a/src/routes/premium-routes.js b/src/routes/premium-routes.js new file mode 100644 index 0000000..8a0e51a --- /dev/null +++ b/src/routes/premium-routes.js @@ -0,0 +1,67 @@ +export function registerPremiumRoutes(app, deps) { + const { + pricingConfig, + broadcast, + runResearch, + runSummary, + runAnalysis, + runCode, + MODEL_LABELS, + } = deps; + + app.get('/api/premium/research', async (req, res, next) => { + try { + const topic = req.query.topic || 'AI and blockchain payments'; + const priceInfo = pricingConfig.getEndpointInfo('GET /api/premium/research'); + const cost = priceInfo.price.slice(1); + broadcast({ type: 'agent_call', agent: `${priceInfo.emoji} Research Agent`, agentId: 'research-bot', input: topic, cost, timestamp: new Date().toISOString() }); + const result = await runResearch(topic); + broadcast({ type: 'agent_response', agent: `${priceInfo.emoji} Research Agent`, agentId: 'research-bot', resultPreview: result.substring(0, 150), cost, timestamp: new Date().toISOString() }); + res.json({ agent: 'research-bot', topic, result, model: MODEL_LABELS.research, cost: `${cost} USDC`, paidVia: 'x402' }); + } catch (err) { + next(err); + } + }); + + app.get('/api/premium/summarize', async (req, res, next) => { + try { + const text = req.query.text || 'Please provide text to summarize via ?text= parameter'; + const priceInfo = pricingConfig.getEndpointInfo('GET /api/premium/summarize'); + const cost = priceInfo.price.slice(1); + broadcast({ type: 'agent_call', agent: `${priceInfo.emoji} Summary Agent`, agentId: 'summary-bot', input: text.substring(0, 100), cost, timestamp: new Date().toISOString() }); + const result = await runSummary(text); + broadcast({ type: 'agent_response', agent: `${priceInfo.emoji} Summary Agent`, agentId: 'summary-bot', resultPreview: result.substring(0, 150), cost, timestamp: new Date().toISOString() }); + res.json({ agent: 'summary-bot', result, model: MODEL_LABELS.summary, cost: `${cost} USDC`, paidVia: 'x402' }); + } catch (err) { + next(err); + } + }); + + app.get('/api/premium/analyze', async (req, res, next) => { + try { + const topic = req.query.topic || 'AI agent economies'; + const priceInfo = pricingConfig.getEndpointInfo('GET /api/premium/analyze'); + const cost = priceInfo.price.slice(1); + broadcast({ type: 'agent_call', agent: `${priceInfo.emoji} Analysis Agent`, agentId: 'analyst-bot', input: topic, cost, timestamp: new Date().toISOString() }); + const result = await runAnalysis(topic); + broadcast({ type: 'agent_response', agent: `${priceInfo.emoji} Analysis Agent`, agentId: 'analyst-bot', resultPreview: result.substring(0, 150), cost, timestamp: new Date().toISOString() }); + res.json({ agent: 'analyst-bot', topic, result, model: MODEL_LABELS.analysis, cost: `${cost} USDC`, paidVia: 'x402' }); + } catch (err) { + next(err); + } + }); + + app.get('/api/premium/code', async (req, res, next) => { + try { + const prompt = req.query.prompt || 'Write a hello world function'; + const priceInfo = pricingConfig.getEndpointInfo('GET /api/premium/code'); + const cost = priceInfo.price.slice(1); + broadcast({ type: 'agent_call', agent: `${priceInfo.emoji} Code Agent`, agentId: 'code-bot', input: prompt.substring(0, 100), cost, timestamp: new Date().toISOString() }); + const result = await runCode(prompt); + broadcast({ type: 'agent_response', agent: `${priceInfo.emoji} Code Agent`, agentId: 'code-bot', resultPreview: result.substring(0, 150), cost, timestamp: new Date().toISOString() }); + res.json({ agent: 'code-bot', prompt, result, model: MODEL_LABELS.code, cost: `${cost} USDC`, paidVia: 'x402' }); + } catch (err) { + next(err); + } + }); +} diff --git a/src/server.js b/src/server.js index f6643d6..43cb3bb 100644 --- a/src/server.js +++ b/src/server.js @@ -12,6 +12,7 @@ import { orchestrate } from './agents/orchestrator.js'; import { getBalance, getTransactions, sendPayment } from './stellar/wallet.js'; import { requestId, errorHandler } from './middleware/errorHandler.js'; import { orchestrateLimiter, apikeyLimiter } from './middleware/rateLimiter.js'; +import { registerPremiumRoutes } from './routes/premium-routes.js'; // x402 imports import { paymentMiddlewareFromConfig } from '@x402/express'; @@ -146,64 +147,17 @@ if (config.serverAddress) { console.warn('⚠️ No SERVER_STELLAR_ADDRESS set — x402 paywall disabled'); } -// ─── Premium x402-Protected Endpoints ──────────────────────── -app.get('/api/premium/research', async (req, res, next) => { - try { - const topic = req.query.topic || 'AI and blockchain payments'; - const priceInfo = pricingConfig.getEndpointInfo('GET /api/premium/research'); - const cost = priceInfo.price.slice(1); // Remove '$' for display - broadcast({ type: 'agent_call', agent: `${priceInfo.emoji} Research Agent`, agentId: 'research-bot', input: topic, cost, timestamp: new Date().toISOString() }); - const result = await runResearch(topic); - broadcast({ type: 'agent_response', agent: `${priceInfo.emoji} Research Agent`, agentId: 'research-bot', resultPreview: result.substring(0, 150), cost, timestamp: new Date().toISOString() }); - res.json({ agent: 'research-bot', topic, result, model: MODEL_LABELS.research, cost: `${cost} USDC`, paidVia: 'x402' }); - } catch (err) { - next(err); - } -}); - -app.get('/api/premium/summarize', async (req, res, next) => { - try { - const text = req.query.text || 'Please provide text to summarize via ?text= parameter'; - const priceInfo = pricingConfig.getEndpointInfo('GET /api/premium/summarize'); - const cost = priceInfo.price.slice(1); - broadcast({ type: 'agent_call', agent: `${priceInfo.emoji} Summary Agent`, agentId: 'summary-bot', input: text.substring(0, 100), cost, timestamp: new Date().toISOString() }); - const result = await runSummary(text); - broadcast({ type: 'agent_response', agent: `${priceInfo.emoji} Summary Agent`, agentId: 'summary-bot', resultPreview: result.substring(0, 150), cost, timestamp: new Date().toISOString() }); - res.json({ agent: 'summary-bot', result, model: MODEL_LABELS.summary, cost: `${cost} USDC`, paidVia: 'x402' }); - } catch (err) { - next(err); - } -}); - -app.get('/api/premium/analyze', async (req, res, next) => { - try { - const topic = req.query.topic || 'AI agent economies'; - const priceInfo = pricingConfig.getEndpointInfo('GET /api/premium/analyze'); - const cost = priceInfo.price.slice(1); - broadcast({ type: 'agent_call', agent: `${priceInfo.emoji} Analysis Agent`, agentId: 'analyst-bot', input: topic, cost, timestamp: new Date().toISOString() }); - const result = await runAnalysis(topic); - broadcast({ type: 'agent_response', agent: `${priceInfo.emoji} Analysis Agent`, agentId: 'analyst-bot', resultPreview: result.substring(0, 150), cost, timestamp: new Date().toISOString() }); - res.json({ agent: 'analyst-bot', topic, result, model: MODEL_LABELS.analysis, cost: `${cost} USDC`, paidVia: 'x402' }); - } catch (err) { - next(err); - } -}); - -app.get('/api/premium/code', async (req, res, next) => { - try { - const prompt = req.query.prompt || 'Write a hello world function'; - const priceInfo = pricingConfig.getEndpointInfo('GET /api/premium/code'); - const cost = priceInfo.price.slice(1); - broadcast({ type: 'agent_call', agent: `${priceInfo.emoji} Code Agent`, agentId: 'code-bot', input: prompt.substring(0, 100), cost, timestamp: new Date().toISOString() }); - const result = await runCode(prompt); - broadcast({ type: 'agent_response', agent: `${priceInfo.emoji} Code Agent`, agentId: 'code-bot', resultPreview: result.substring(0, 150), cost, timestamp: new Date().toISOString() }); - res.json({ agent: 'code-bot', prompt, result, model: MODEL_LABELS.code, cost: `${cost} USDC`, paidVia: 'x402' }); - } catch (err) { - next(err); - } +// Premium x402-Protected Endpoints +registerPremiumRoutes(app, { + pricingConfig, + broadcast, + runResearch, + runSummary, + runAnalysis, + runCode, + MODEL_LABELS, }); -// ─── Free Agent Endpoints (for internal orchestrator use) ──── app.get('/api/research', async (req, res, next) => { try { const topic = req.query.topic || 'AI payments'; diff --git a/tests/premium-endpoints.integration.test.js b/tests/premium-endpoints.integration.test.js new file mode 100644 index 0000000..06d0226 --- /dev/null +++ b/tests/premium-endpoints.integration.test.js @@ -0,0 +1,140 @@ +import assert from 'node:assert'; +import express from 'express'; +import { pricingConfig } from '../src/pricing.config.js'; +import { registerPremiumRoutes } from '../src/routes/premium-routes.js'; + +function createTestApp({ withPaymentHeaderRequired = true } = {}) { + const app = express(); + + // Mock payment middleware: block premium endpoints unless payment header exists. + app.use((req, res, next) => { + if (!req.path.startsWith('/api/premium/')) return next(); + if (!withPaymentHeaderRequired) return next(); + + if (!req.header('x-payment-context')) { + return res.status(402).json({ + error: 'payment_required', + message: 'Missing payment context', + }); + } + return next(); + }); + + const calls = { + research: 0, + summary: 0, + analysis: 0, + code: 0, + }; + + registerPremiumRoutes(app, { + pricingConfig, + broadcast: () => {}, + runResearch: async (topic) => { + calls.research += 1; + return `research:${topic}`; + }, + runSummary: async (text) => { + calls.summary += 1; + return `summary:${text}`; + }, + runAnalysis: async (topic) => { + calls.analysis += 1; + return `analysis:${topic}`; + }, + runCode: async (prompt) => { + calls.code += 1; + return `code:${prompt}`; + }, + MODEL_LABELS: { + research: 'test-research-model', + summary: 'test-summary-model', + analysis: 'test-analysis-model', + code: 'test-code-model', + }, + }); + + return { app, calls }; +} + +async function requestJson(baseUrl, endpoint, payment = false) { + const headers = payment ? { 'x-payment-context': 'paid' } : {}; + const res = await fetch(`${baseUrl}${endpoint}`, { headers }); + const body = await res.json(); + return { status: res.status, body }; +} + +async function withServer(app, fn) { + const server = await new Promise((resolve) => { + const s = app.listen(0, () => resolve(s)); + }); + const { port } = server.address(); + const baseUrl = `http://127.0.0.1:${port}`; + try { + await fn(baseUrl); + } finally { + await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))); + } +} + +async function run() { + const endpoints = [ + '/api/premium/research?topic=test-topic', + '/api/premium/summarize?text=test-text', + '/api/premium/analyze?topic=test-topic', + '/api/premium/code?prompt=test-prompt', + ]; + + // Without payment context => blocked by middleware + { + const { app, calls } = createTestApp(); + await withServer(app, async (baseUrl) => { + for (const endpoint of endpoints) { + const { status, body } = await requestJson(baseUrl, endpoint, false); + assert.strictEqual(status, 402, `Expected 402 for ${endpoint} without payment context`); + assert.strictEqual(body.error, 'payment_required', `Expected payment_required payload for ${endpoint}`); + } + }); + assert.deepStrictEqual(calls, { research: 0, summary: 0, analysis: 0, code: 0 }, 'Agent handlers must not execute on 402'); + } + + // With payment context => premium handlers succeed + { + const { app, calls } = createTestApp(); + await withServer(app, async (baseUrl) => { + const research = await requestJson(baseUrl, '/api/premium/research?topic=my-topic', true); + assert.strictEqual(research.status, 200); + assert.strictEqual(research.body.agent, 'research-bot'); + assert.strictEqual(research.body.paidVia, 'x402'); + assert.strictEqual(research.body.cost, '0.01 USDC'); + + const summarize = await requestJson(baseUrl, '/api/premium/summarize?text=my-text', true); + assert.strictEqual(summarize.status, 200); + assert.strictEqual(summarize.body.agent, 'summary-bot'); + assert.strictEqual(summarize.body.paidVia, 'x402'); + assert.strictEqual(summarize.body.cost, '0.01 USDC'); + + const analyze = await requestJson(baseUrl, '/api/premium/analyze?topic=my-topic', true); + assert.strictEqual(analyze.status, 200); + assert.strictEqual(analyze.body.agent, 'analyst-bot'); + assert.strictEqual(analyze.body.paidVia, 'x402'); + assert.strictEqual(analyze.body.cost, '0.05 USDC'); + + const code = await requestJson(baseUrl, '/api/premium/code?prompt=my-prompt', true); + assert.strictEqual(code.status, 200); + assert.strictEqual(code.body.agent, 'code-bot'); + assert.strictEqual(code.body.paidVia, 'x402'); + assert.strictEqual(code.body.cost, '0.03 USDC'); + }); + + assert.deepStrictEqual(calls, { research: 1, summary: 1, analysis: 1, code: 1 }, 'Each premium handler should run exactly once with payment'); + } + + console.log('✅ premium endpoint integration tests passed'); +} + +run().catch((err) => { + console.error('❌ premium endpoint integration tests failed'); + console.error(err); + process.exit(1); +});