479 lines
14 KiB
TypeScript
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();
|