#!/usr/bin/env tsx /** * Pangolin API Endpoint Testing Script * * Tests all critical endpoints used by automated setup to verify Pangolin instance compatibility. * Run with: npx tsx api/test-pangolin-endpoints.ts */ import { env } from './src/config/env'; import { pangolinClient } from './src/services/pangolin.client'; import { logger } from './src/utils/logger'; interface TestResult { name: string; endpoint: string; status: 'pass' | 'fail' | 'warn' | 'skip'; message: string; data?: unknown; error?: string; } const results: TestResult[] = []; function logResult(result: TestResult) { const emoji = result.status === 'pass' ? '✅' : result.status === 'fail' ? '❌' : result.status === 'warn' ? '⚠️' : '⏭️'; console.log(`${emoji} ${result.name}: ${result.message}`); if (result.error) { console.log(` Error: ${result.error}`); } results.push(result); } async function testHealthCheck() { console.log('\n=== Testing Health & Configuration ===\n'); // Test 1: Configuration check if (!pangolinClient.configured) { logResult({ name: 'Configuration', endpoint: 'N/A', status: 'fail', message: 'Pangolin not configured', error: 'Missing required env vars: PANGOLIN_API_URL, PANGOLIN_API_KEY, or PANGOLIN_ORG_ID' }); return false; } logResult({ name: 'Configuration', endpoint: 'N/A', status: 'pass', message: `Configured with API URL: ${env.PANGOLIN_API_URL}, Org ID: ${env.PANGOLIN_ORG_ID}` }); // Test 2: API health check try { const healthy = await pangolinClient.healthCheck(); if (healthy) { logResult({ name: 'API Health', endpoint: 'GET /', status: 'pass', message: 'API is reachable' }); return true; } else { logResult({ name: 'API Health', endpoint: 'GET /', status: 'fail', message: 'API returned non-200 status' }); return false; } } catch (err) { logResult({ name: 'API Health', endpoint: 'GET /', status: 'fail', message: 'Failed to connect to API', error: err instanceof Error ? err.message : String(err) }); return false; } } async function testSiteEndpoints() { console.log('\n=== Testing Site Endpoints ===\n'); // Test 1: List sites try { const sites = await pangolinClient.listSites(); logResult({ name: 'List Sites', endpoint: `GET /org/${env.PANGOLIN_ORG_ID}/sites`, status: 'pass', message: `Found ${sites.length} sites`, data: sites.map(s => ({ siteId: s.siteId, name: s.name, subnet: s.subnet })) }); // Test 2: Get site details (if sites exist) if (sites.length > 0 && sites[0]) { const siteId = sites[0].siteId; try { const site = await pangolinClient.getSite(siteId); logResult({ name: 'Get Site Details', endpoint: `GET /site/${siteId}`, status: 'pass', message: `Retrieved site: ${site.name}`, data: { siteId: site.siteId, name: site.name, online: site.online, subnet: site.subnet } }); } catch (err) { logResult({ name: 'Get Site Details', endpoint: `GET /site/${siteId}`, status: 'fail', message: 'Failed to get site details', error: err instanceof Error ? err.message : String(err) }); } } else { logResult({ name: 'Get Site Details', endpoint: 'GET /site/{siteId}', status: 'skip', message: 'No sites available to test' }); } } catch (err) { logResult({ name: 'List Sites', endpoint: `GET /org/${env.PANGOLIN_ORG_ID}/sites`, status: 'fail', message: 'Failed to list sites', error: err instanceof Error ? err.message : String(err) }); } } async function testExitNodeEndpoints() { console.log('\n=== Testing Exit Node Endpoints ===\n'); try { const exitNodes = await pangolinClient.listExitNodes(); if (exitNodes.length > 0) { logResult({ name: 'List Exit Nodes', endpoint: `GET /org/${env.PANGOLIN_ORG_ID}/exit-nodes`, status: 'pass', message: `Found ${exitNodes.length} exit nodes`, data: exitNodes.map(n => ({ exitNodeId: n.exitNodeId, name: n.name, online: n.online, location: n.location })) }); } else { logResult({ name: 'List Exit Nodes', endpoint: `GET /org/${env.PANGOLIN_ORG_ID}/exit-nodes`, status: 'warn', message: 'No exit nodes available (self-hosted mode)', data: [] }); } } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); if (errorMsg.includes('404')) { logResult({ name: 'List Exit Nodes', endpoint: `GET /org/${env.PANGOLIN_ORG_ID}/exit-nodes`, status: 'warn', message: 'Endpoint not available (404) - self-hosted mode without separate exit nodes', error: 'This is expected for self-hosted Pangolin instances' }); } else { logResult({ name: 'List Exit Nodes', endpoint: `GET /org/${env.PANGOLIN_ORG_ID}/exit-nodes`, status: 'fail', message: 'Failed to list exit nodes', error: errorMsg }); } } } async function testDomainEndpoints() { console.log('\n=== Testing Domain Endpoints ===\n'); try { const domains = await pangolinClient.listDomains(); if (domains.length > 0) { logResult({ name: 'List Domains', endpoint: `GET /org/${env.PANGOLIN_ORG_ID}/domains`, status: 'pass', message: `Found ${domains.length} domains`, data: domains.map(d => ({ domainId: d.domainId, baseDomain: d.baseDomain, verified: d.verified, type: d.type })) }); // Check if DOMAIN env var matches any registered domain const envDomain = env.DOMAIN; const matchingDomain = domains.find(d => envDomain.endsWith(d.baseDomain)); if (matchingDomain) { logResult({ name: 'Domain Match', endpoint: 'N/A', status: 'pass', message: `Environment DOMAIN (${envDomain}) matches registered domain: ${matchingDomain.baseDomain}`, data: { domainId: matchingDomain.domainId, verified: matchingDomain.verified } }); } else { logResult({ name: 'Domain Match', endpoint: 'N/A', status: 'warn', message: `Environment DOMAIN (${envDomain}) does not match any registered domains`, error: `Available domains: ${domains.map(d => d.baseDomain).join(', ')}` }); } } else { logResult({ name: 'List Domains', endpoint: `GET /org/${env.PANGOLIN_ORG_ID}/domains`, status: 'fail', message: 'No domains registered', error: 'You must register a domain in Pangolin dashboard before running automated setup' }); } } catch (err) { logResult({ name: 'List Domains', endpoint: `GET /org/${env.PANGOLIN_ORG_ID}/domains`, status: 'fail', message: 'Failed to list domains', error: err instanceof Error ? err.message : String(err) }); } } async function testResourceEndpoints() { console.log('\n=== Testing Resource Endpoints ===\n'); try { const resources = await pangolinClient.listResources(); logResult({ name: 'List Site Resources', endpoint: `GET /org/${env.PANGOLIN_ORG_ID}/site-resources`, status: 'pass', message: `Found ${resources.length} resources`, data: resources.slice(0, 5).map(r => ({ resourceId: r.resourceId, name: r.name, fullDomain: r.fullDomain, ssl: r.ssl, active: r.active })) }); // Test get resource details if any exist if (resources.length > 0 && resources[0]) { const resourceId = resources[0].resourceId; try { const resource = await pangolinClient.getResource(resourceId); logResult({ name: 'Get Resource Details', endpoint: `GET /site-resource/${resourceId}`, status: 'pass', message: `Retrieved resource: ${resource.name}`, data: { resourceId: resource.resourceId, name: resource.name, fullDomain: resource.fullDomain, ssl: resource.ssl } }); } catch (err) { logResult({ name: 'Get Resource Details', endpoint: `GET /site-resource/${resourceId}`, status: 'fail', message: 'Failed to get resource details', error: err instanceof Error ? err.message : String(err) }); } } else { logResult({ name: 'Get Resource Details', endpoint: 'GET /site-resource/{resourceId}', status: 'skip', message: 'No resources available to test' }); } } catch (err) { logResult({ name: 'List Site Resources', endpoint: `GET /org/${env.PANGOLIN_ORG_ID}/site-resources`, status: 'fail', message: 'Failed to list resources', error: err instanceof Error ? err.message : String(err) }); } } async function testCreateResourceFlow() { console.log('\n=== Testing Resource Creation Flow (Read-Only Validation) ===\n'); // Validate we have the required data to create resources const domains = await pangolinClient.listDomains().catch(() => []); const sites = await pangolinClient.listSites().catch(() => []); if (domains.length === 0) { logResult({ name: 'Resource Creation Pre-Check', endpoint: 'N/A', status: 'fail', message: 'Cannot create resources - no domains registered', error: 'Register a domain in Pangolin dashboard first' }); return; } if (sites.length === 0) { logResult({ name: 'Resource Creation Pre-Check', endpoint: 'N/A', status: 'warn', message: 'No sites exist - automated setup will create one', data: { note: 'First run of automated setup will create a new site', recommendedSubnet: '100.89.128.4/30' } }); } else { logResult({ name: 'Resource Creation Pre-Check', endpoint: 'N/A', status: 'pass', message: `Ready to create resources - ${sites.length} sites available`, data: sites.map(s => ({ siteId: s.siteId, name: s.name, subnet: s.subnet })) }); } // Validate domain match const envDomain = env.DOMAIN; const matchingDomain = domains.find(d => envDomain.endsWith(d.baseDomain)); if (matchingDomain) { logResult({ name: 'Domain Validation', endpoint: 'N/A', status: 'pass', message: `Domain ${envDomain} is registered and ready`, data: { domainId: matchingDomain.domainId, baseDomain: matchingDomain.baseDomain } }); } else { logResult({ name: 'Domain Validation', endpoint: 'N/A', status: 'fail', message: `Domain ${envDomain} not found in Pangolin`, error: `Register ${envDomain} in Pangolin dashboard. Available: ${domains.map(d => d.baseDomain).join(', ')}` }); } } function printSummary() { console.log('\n' + '='.repeat(60)); console.log('TEST SUMMARY'); console.log('='.repeat(60) + '\n'); const passed = results.filter(r => r.status === 'pass').length; const failed = results.filter(r => r.status === 'fail').length; const warned = results.filter(r => r.status === 'warn').length; const skipped = results.filter(r => r.status === 'skip').length; console.log(`Total Tests: ${results.length}`); console.log(`✅ Passed: ${passed}`); console.log(`❌ Failed: ${failed}`); console.log(`⚠️ Warnings: ${warned}`); console.log(`⏭️ Skipped: ${skipped}\n`); if (failed > 0) { console.log('❌ CRITICAL FAILURES:\n'); results.filter(r => r.status === 'fail').forEach(r => { console.log(` - ${r.name} (${r.endpoint})`); console.log(` ${r.message}`); if (r.error) console.log(` Error: ${r.error}`); console.log(''); }); } if (warned > 0) { console.log('⚠️ WARNINGS:\n'); results.filter(r => r.status === 'warn').forEach(r => { console.log(` - ${r.name} (${r.endpoint})`); console.log(` ${r.message}`); if (r.error) console.log(` ${r.error}`); console.log(''); }); } // Overall verdict console.log('='.repeat(60)); if (failed === 0) { console.log('✅ ALL CRITICAL TESTS PASSED'); console.log(' Your Pangolin instance is ready for automated setup!'); } else { console.log('❌ SOME TESTS FAILED'); console.log(' Fix the critical failures above before running automated setup.'); } console.log('='.repeat(60) + '\n'); // Export detailed results to file const fs = require('fs'); const path = require('path'); const outputPath = path.join(process.cwd(), 'pangolin-test-results.json'); fs.writeFileSync(outputPath, JSON.stringify({ timestamp: new Date().toISOString(), summary: { total: results.length, passed, failed, warned, skipped }, results }, null, 2)); console.log(`📄 Detailed results saved to: ${outputPath}\n`); } async function main() { console.log('🔍 Pangolin API Endpoint Testing'); console.log('='.repeat(60)); console.log(`API URL: ${env.PANGOLIN_API_URL}`); console.log(`Org ID: ${env.PANGOLIN_ORG_ID}`); console.log(`Domain: ${env.DOMAIN}`); console.log('='.repeat(60)); try { // Test 1: Health & Configuration const healthy = await testHealthCheck(); if (!healthy) { console.log('\n❌ API health check failed - skipping remaining tests\n'); printSummary(); process.exit(1); } // Test 2: Site Endpoints await testSiteEndpoints(); // Test 3: Exit Node Endpoints (optional) await testExitNodeEndpoints(); // Test 4: Domain Endpoints await testDomainEndpoints(); // Test 5: Resource Endpoints await testResourceEndpoints(); // Test 6: Resource Creation Flow Validation await testCreateResourceFlow(); // Print summary printSummary(); // Exit with appropriate code const failed = results.filter(r => r.status === 'fail').length; process.exit(failed > 0 ? 1 : 0); } catch (err) { console.error('\n❌ Unexpected error during testing:', err); process.exit(1); } } main();