changemaker.lite/api/test-pangolin-endpoints.ts

479 lines
14 KiB
TypeScript

#!/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();