#!/usr/bin/env node /** * API Endpoints UI Tests * * Tests for all major API endpoints via Puppeteer's page.evaluate(fetch...). * * Run: node test_api_endpoints_ci.js */ const { setupTest, teardownTest, TestResults, log, navigateTo, withTimeout } = require('./test_lib'); // ============================================================================ // Research API Tests // ============================================================================ const ResearchApiTests = { async apiHealthEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/api/v1/health`); const data = await response.json(); return { ok: response.ok, status: response.status, hasStatus: 'status' in data, statusValue: data.status }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); return { passed: result.ok && result.status === 200, message: result.ok ? `Health endpoint OK (status: ${result.statusValue})` : `Health endpoint failed: ${result.error || 'status ' + result.status}` }; }, async apiDocumentationEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/api/v1/`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); return { ok: true, status: response.status, hasEndpoints: 'endpoints' in data || Array.isArray(data) || Object.keys(data).length > 0 }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); return { passed: result.ok, message: result.ok ? `API documentation endpoint OK (hasEndpoints: ${result.hasEndpoints})` : `API docs failed: ${result.error || 'status ' + result.status}` }; }, async currentConfigEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/research/api/settings/current-config`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); return { ok: true, status: response.status, hasProvider: 'provider' in data || 'llm_provider' in data, hasModel: 'model' in data || 'model_name' in data }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Current config endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Current config endpoint OK (provider: ${result.hasProvider}, model: ${result.hasModel})` : `Config endpoint failed: ${result.error || 'status ' + result.status}` }; }, async availableModelsEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/settings/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/settings/api/available-models`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); const modelCount = Array.isArray(data) ? data.length : (data.models ? data.models.length : Object.keys(data).length); return { ok: true, status: response.status, modelCount }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); return { passed: result.ok, message: result.ok ? `Available models endpoint OK (${result.modelCount} models)` : `Models endpoint failed: ${result.error || 'status ' + result.status}` }; }, async availableSearchEnginesEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/settings/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/settings/api/available-search-engines`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); const engineCount = Array.isArray(data) ? data.length : (data.engines ? data.engines.length : Object.keys(data).length); return { ok: true, status: response.status, engineCount }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); return { passed: result.ok, message: result.ok ? `Available search engines endpoint OK (${result.engineCount} engines)` : `Engines endpoint failed: ${result.error || 'status ' + result.status}` }; }, async ollamaStatusEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/settings/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/settings/api/ollama-status`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); return { ok: true, status: response.status, running: data.running || data.available || data.status === 'ok', hasModelCount: 'model_count' in data || 'models' in data }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Ollama status endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Ollama status endpoint OK (running: ${result.running})` : `Ollama status failed: ${result.error || 'status ' + result.status}` }; }, async historyApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/history`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/api/history`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); const itemCount = Array.isArray(data) ? data.length : (data.items ? data.items.length : 0); return { ok: true, status: response.status, itemCount }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); return { passed: result.ok, message: result.ok ? `History API endpoint OK (${result.itemCount} items)` : `History API failed: ${result.error || 'status ' + result.status}` }; }, async queueStatusEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/api/queue/status`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); return { ok: true, status: response.status, hasQueueInfo: 'pending' in data || 'queue_length' in data || 'active' in data }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Queue status endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Queue status endpoint OK (hasQueueInfo: ${result.hasQueueInfo})` : `Queue status failed: ${result.error || 'status ' + result.status}` }; } }; // ============================================================================ // Metrics API Tests // ============================================================================ const MetricsApiTests = { async metricsApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/metrics/api/metrics`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); return { ok: true, status: response.status, keys: Object.keys(data).slice(0, 5) }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); return { passed: result.ok, message: result.ok ? `Metrics API endpoint OK (keys: ${result.keys.join(', ')})` : `Metrics API failed: ${result.error || 'status ' + result.status}` }; }, async enhancedMetricsEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/metrics/api/metrics/enhanced`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); return { ok: true, status: response.status, hasData: Object.keys(data).length > 0 }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Enhanced metrics endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Enhanced metrics endpoint OK` : `Enhanced metrics failed: ${result.error || 'status ' + result.status}` }; }, async pricingApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/costs`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/metrics/api/pricing`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); const modelCount = Array.isArray(data) ? data.length : Object.keys(data).length; return { ok: true, status: response.status, modelCount }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Pricing API endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Pricing API endpoint OK (${result.modelCount} models)` : `Pricing API failed: ${result.error || 'status ' + result.status}` }; }, async rateLimitingApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/metrics/api/rate-limiting`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); return { ok: true, status: response.status, hasData: Object.keys(data).length > 0 }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Rate limiting API endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Rate limiting API endpoint OK` : `Rate limiting API failed: ${result.error || 'status ' + result.status}` }; }, async linkAnalyticsApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/links`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/metrics/api/link-analytics`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); return { ok: true, status: response.status, hasData: Object.keys(data).length > 0 }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Link analytics API endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Link analytics API endpoint OK` : `Link analytics API failed: ${result.error || 'status ' + result.status}` }; }, async starReviewsApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/star-reviews`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/metrics/api/star-reviews`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); return { ok: true, status: response.status, hasData: Object.keys(data).length > 0 }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Star reviews API endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Star reviews API endpoint OK` : `Star reviews API failed: ${result.error || 'status ' + result.status}` }; } }; // ============================================================================ // News API Tests // ============================================================================ const NewsApiTests = { async newsFeedApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/news`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/api/news/feed`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); const itemCount = Array.isArray(data) ? data.length : (data.items ? data.items.length : 0); return { ok: true, status: response.status, itemCount }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'News feed API endpoint not found' }; } return { passed: result.ok, message: result.ok ? `News feed API endpoint OK (${result.itemCount} items)` : `News feed API failed: ${result.error || 'status ' + result.status}` }; }, async subscriptionsApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/news/subscriptions`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/api/news/subscriptions`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); const subCount = Array.isArray(data) ? data.length : (data.subscriptions ? data.subscriptions.length : 0); return { ok: true, status: response.status, subCount }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Subscriptions API endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Subscriptions API endpoint OK (${result.subCount} subscriptions)` : `Subscriptions API failed: ${result.error || 'status ' + result.status}` }; }, async categoriesApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/news`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/api/news/categories`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); const catCount = Array.isArray(data) ? data.length : Object.keys(data).length; return { ok: true, status: response.status, catCount }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && (result.status === 404 || result.status === 501)) { return { passed: null, skipped: true, message: `Categories API endpoint not available (status ${result.status})` }; } return { passed: result.ok, message: result.ok ? `Categories API endpoint OK (${result.catCount} categories)` : `Categories API failed: ${result.error || 'status ' + result.status}` }; } }; // ============================================================================ // Library API Tests // ============================================================================ const LibraryApiTests = { async documentsApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/library`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/library/api/documents`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); const docCount = Array.isArray(data) ? data.length : (data.documents ? data.documents.length : 0); return { ok: true, status: response.status, docCount }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Documents API endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Documents API endpoint OK (${result.docCount} documents)` : `Documents API failed: ${result.error || 'status ' + result.status}` }; }, async collectionsListApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/library`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/library/api/collections/list`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); const collCount = Array.isArray(data) ? data.length : (data.collections ? data.collections.length : 0); return { ok: true, status: response.status, collCount }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Collections list API endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Collections list API endpoint OK (${result.collCount} collections)` : `Collections API failed: ${result.error || 'status ' + result.status}` }; } }; // ============================================================================ // Context Overflow API Tests // ============================================================================ const ContextOverflowApiTests = { async contextOverflowApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/context-overflow`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/metrics/api/context-overflow`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); return { ok: true, status: response.status, hasData: Object.keys(data).length > 0, hasTruncationRate: 'truncation_rate' in data || 'rate' in data }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Context overflow API endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Context overflow API endpoint OK (hasTruncationRate: ${result.hasTruncationRate})` : `Context overflow API failed: ${result.error || 'status ' + result.status}` }; } }; // ============================================================================ // Settings API Tests // ============================================================================ const SettingsApiTests = { async settingsApiEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/settings/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/settings/api`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); const settingCount = Object.keys(data).length; return { ok: true, status: response.status, settingCount }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); return { passed: result.ok, message: result.ok ? `Settings API endpoint OK (${result.settingCount} settings)` : `Settings API failed: ${result.error || 'status ' + result.status}` }; }, async settingsWarningsEndpoint(page, baseUrl) { await navigateTo(page, `${baseUrl}/settings/`); const result = await page.evaluate(async (url) => { try { const response = await fetch(`${url}/settings/api/warnings`); if (!response.ok) return { ok: false, status: response.status }; const data = await response.json(); const warningCount = Array.isArray(data) ? data.length : (data.warnings ? data.warnings.length : 0); return { ok: true, status: response.status, warningCount }; } catch (e) { return { ok: false, error: e.message }; } }, baseUrl); if (!result.ok && result.status === 404) { return { passed: null, skipped: true, message: 'Settings warnings endpoint not found' }; } return { passed: result.ok, message: result.ok ? `Settings warnings endpoint OK (${result.warningCount} warnings)` : `Settings warnings failed: ${result.error || 'status ' + result.status}` }; } }; // ============================================================================ // Main Test Runner // ============================================================================ async function main() { log.section('API Endpoints Tests'); const ctx = await setupTest({ authenticate: true }); const results = new TestResults('API Endpoints Tests'); const { page } = ctx; const { baseUrl } = ctx.config; const subTestTimeout = ctx.config.isCI ? 60000 : 30000; async function run(category, name, testFn) { try { const result = await withTimeout( testFn(page, baseUrl), subTestTimeout, `${category}/${name}` ); if (result.skipped) { results.skip(category, name, result.message); } else { results.add(category, name, result.passed, result.message); } } catch (error) { results.add(category, name, false, `Error: ${error.message}`); } } try { // Research API Tests log.section('Research APIs'); await run('Research', 'API Health Endpoint', (p, u) => ResearchApiTests.apiHealthEndpoint(p, u)); await run('Research', 'API Documentation Endpoint', (p, u) => ResearchApiTests.apiDocumentationEndpoint(p, u)); await run('Research', 'Current Config Endpoint', (p, u) => ResearchApiTests.currentConfigEndpoint(p, u)); await run('Research', 'Available Models Endpoint', (p, u) => ResearchApiTests.availableModelsEndpoint(p, u)); await run('Research', 'Available Search Engines Endpoint', (p, u) => ResearchApiTests.availableSearchEnginesEndpoint(p, u)); await run('Research', 'Ollama Status Endpoint', (p, u) => ResearchApiTests.ollamaStatusEndpoint(p, u)); await run('Research', 'History API Endpoint', (p, u) => ResearchApiTests.historyApiEndpoint(p, u)); await run('Research', 'Queue Status Endpoint', (p, u) => ResearchApiTests.queueStatusEndpoint(p, u)); // Metrics API Tests log.section('Metrics APIs'); await run('Metrics', 'Metrics API Endpoint', (p, u) => MetricsApiTests.metricsApiEndpoint(p, u)); await run('Metrics', 'Enhanced Metrics Endpoint', (p, u) => MetricsApiTests.enhancedMetricsEndpoint(p, u)); await run('Metrics', 'Pricing API Endpoint', (p, u) => MetricsApiTests.pricingApiEndpoint(p, u)); await run('Metrics', 'Rate Limiting API Endpoint', (p, u) => MetricsApiTests.rateLimitingApiEndpoint(p, u)); await run('Metrics', 'Link Analytics API Endpoint', (p, u) => MetricsApiTests.linkAnalyticsApiEndpoint(p, u)); await run('Metrics', 'Star Reviews API Endpoint', (p, u) => MetricsApiTests.starReviewsApiEndpoint(p, u)); // News API Tests log.section('News APIs'); await run('News', 'News Feed API Endpoint', (p, u) => NewsApiTests.newsFeedApiEndpoint(p, u)); await run('News', 'Subscriptions API Endpoint', (p, u) => NewsApiTests.subscriptionsApiEndpoint(p, u)); await run('News', 'Categories API Endpoint', (p, u) => NewsApiTests.categoriesApiEndpoint(p, u)); // Library API Tests log.section('Library APIs'); await run('Library', 'Documents API Endpoint', (p, u) => LibraryApiTests.documentsApiEndpoint(p, u)); await run('Library', 'Collections List API Endpoint', (p, u) => LibraryApiTests.collectionsListApiEndpoint(p, u)); // Context Overflow API Tests log.section('Context Overflow API'); await run('Context', 'Context Overflow API Endpoint', (p, u) => ContextOverflowApiTests.contextOverflowApiEndpoint(p, u)); // Settings API Tests log.section('Settings APIs'); await run('Settings', 'Settings API Endpoint', (p, u) => SettingsApiTests.settingsApiEndpoint(p, u)); await run('Settings', 'Settings Warnings Endpoint', (p, u) => SettingsApiTests.settingsWarningsEndpoint(p, u)); } catch (error) { log.error(`Fatal error: ${error.message}`); console.error(error.stack); } finally { results.print(); results.save(); await teardownTest(ctx); process.exit(results.exitCode()); } } // Run if executed directly if (require.main === module) { main().catch(error => { console.error('Test runner failed:', error); process.exit(1); }); } module.exports = { ResearchApiTests, MetricsApiTests, NewsApiTests, LibraryApiTests, ContextOverflowApiTests, SettingsApiTests };