UNPKG

@smartsamurai/krapi-sdk

Version:

KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)

504 lines (459 loc) 14.2 kB
/** * Testing Service for KRAPI SDK * * Provides testing utilities, health checks, and development helpers. */ import crypto from "crypto"; import { DatabaseConnection, Logger } from "./core"; import { KrapiError } from "./core/krapi-error"; import { normalizeError } from "./utils/error-handler"; export interface TestResult { name: string; passed: boolean; message: string; duration: number; details?: Record<string, unknown>; } export interface TestSuite { name: string; tests: TestResult[]; totalPassed: number; totalFailed: number; duration: number; success: boolean; } export interface DatabaseTestResult { connection: boolean; tables: boolean; indexes: boolean; constraints: boolean; performance: boolean; details: Record<string, unknown>; } export interface EndpointTestResult { endpoint: string; method: string; status: number; responseTime: number; success: boolean; error?: string; } /** * Testing Service for KRAPI SDK * * Provides testing utilities, health checks, and development helpers. * * @class TestingService * @example * const testingService = new TestingService(dbConnection, logger); * const testSuite = await testingService.runDatabaseTests(); */ export class TestingService { private db: DatabaseConnection; private logger: Logger; /** * Create a new TestingService instance * * @param {DatabaseConnection} databaseConnection - Database connection * @param {Logger} logger - Logger instance */ constructor(databaseConnection: DatabaseConnection, logger: Logger) { this.db = databaseConnection; this.logger = logger; } /** * Run database tests * * Runs comprehensive database tests including connection, tables, performance, and data integrity. * * @returns {Promise<TestSuite>} Test suite results * * @example * const testSuite = await testingService.runDatabaseTests(); * console.log(`Passed: ${testSuite.totalPassed}, Failed: ${testSuite.totalFailed}`); */ async runDatabaseTests(): Promise<TestSuite> { const startTime = Date.now(); const tests: TestResult[] = []; // Test database connection tests.push(await this.testDatabaseConnection()); // Test required tables tests.push(await this.testRequiredTables()); // Test database performance tests.push(await this.testDatabasePerformance()); // Test data integrity tests.push(await this.testDataIntegrity()); const endTime = Date.now(); const totalPassed = tests.filter((t) => t.passed).length; const totalFailed = tests.length - totalPassed; return { name: "Database Tests", tests, totalPassed, totalFailed, duration: endTime - startTime, success: totalFailed === 0, }; } private async testDatabaseConnection(): Promise<TestResult> { const startTime = Date.now(); try { await this.db.query("SELECT 1 as test"); return { name: "Database Connection", passed: true, message: "Database connection successful", duration: Date.now() - startTime, }; } catch (error) { return { name: "Database Connection", passed: false, message: `Database connection failed: ${error}`, duration: Date.now() - startTime, details: { error }, }; } } private async testRequiredTables(): Promise<TestResult> { const startTime = Date.now(); const requiredTables = [ "admin_users", "projects", "collections", "documents", "api_keys", "sessions", "files", "changelog", ]; try { const missingTables = []; for (const table of requiredTables) { const result = await this.db.query( "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)", [table] ); const exists = (result.rows[0] as { exists: boolean }).exists; if (!exists) { missingTables.push(table); } } const passed = missingTables.length === 0; return { name: "Required Tables", passed, message: passed ? "All required tables exist" : `Missing tables: ${missingTables.join(", ")}`, duration: Date.now() - startTime, details: { missingTables, requiredTables }, }; } catch (error) { return { name: "Required Tables", passed: false, message: `Failed to check tables: ${error}`, duration: Date.now() - startTime, details: { error }, }; } } private async testDatabasePerformance(): Promise<TestResult> { const startTime = Date.now(); try { // Test simple query performance const perfStart = Date.now(); await this.db.query("SELECT COUNT(*) FROM admin_users"); const queryTime = Date.now() - perfStart; const passed = queryTime < 1000; // Should complete within 1 second return { name: "Database Performance", passed, message: passed ? `Query completed in ${queryTime}ms` : `Query too slow: ${queryTime}ms`, duration: Date.now() - startTime, details: { queryTime, threshold: 1000 }, }; } catch (error) { return { name: "Database Performance", passed: false, message: `Performance test failed: ${error}`, duration: Date.now() - startTime, details: { error }, }; } } private async testDataIntegrity(): Promise<TestResult> { const startTime = Date.now(); try { // Check for orphaned records, constraint violations, etc. const issues = []; // Check for projects without valid admin owners const orphanedProjectsResult = await this.db.query(` SELECT COUNT(*) as count FROM projects p LEFT JOIN admin_users a ON p.created_by = a.id WHERE a.id IS NULL AND p.created_by IS NOT NULL `); const orphanedProjects = parseInt( (orphanedProjectsResult.rows[0] as { count: string }).count ); if (orphanedProjects > 0) { issues.push(`${orphanedProjects} projects with invalid owners`); } // Check for collections without valid projects const orphanedCollectionsResult = await this.db.query(` SELECT COUNT(*) as count FROM collections c LEFT JOIN projects p ON c.project_id = p.id WHERE p.id IS NULL `); const orphanedCollections = parseInt( (orphanedCollectionsResult.rows[0] as { count: string }).count ); if (orphanedCollections > 0) { issues.push( `${orphanedCollections} collections without valid projects` ); } const passed = issues.length === 0; return { name: "Data Integrity", passed, message: passed ? "No data integrity issues found" : `Issues found: ${issues.join(", ")}`, duration: Date.now() - startTime, details: { issues, orphanedProjects, orphanedCollections }, }; } catch (error) { return { name: "Data Integrity", passed: false, message: `Data integrity test failed: ${error}`, duration: Date.now() - startTime, details: { error }, }; } } // API Endpoint Testing (for integration tests) async testEndpoint( baseUrl: string, endpoint: string, method = "GET", headers?: Record<string, string>, body?: unknown ): Promise<EndpointTestResult> { const startTime = Date.now(); try { // Use built-in fetch (Node 18+/browsers). If unavailable, throw. const fetchFn: typeof fetch | undefined = ( globalThis as Record<string, unknown> ).fetch as typeof fetch | undefined; if (!fetchFn) { throw KrapiError.validationError( "No fetch implementation available. Please run on Node 18+ or provide a global fetch.", "fetch" ); } const url = `${baseUrl}${endpoint}`; const options: Record<string, unknown> = { method, headers: { "Content-Type": "application/json", ...headers, }, }; if ( body && (method === "POST" || method === "PUT" || method === "PATCH") ) { options.body = JSON.stringify(body); } const response = await fetchFn(url, options); const responseTime = Date.now() - startTime; return { endpoint, method, status: response.status, responseTime, success: response.status >= 200 && response.status < 300, }; } catch (error) { return { endpoint, method, status: 0, responseTime: Date.now() - startTime, success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } // Test Suite Runner async runFullTestSuite(): Promise<{ database: TestSuite; summary: { totalTests: number; totalPassed: number; totalFailed: number; duration: number; success: boolean; }; }> { const startTime = Date.now(); // Run database tests const database = await this.runDatabaseTests(); // Calculate summary const totalTests = database.tests.length; const totalPassed = database.totalPassed; const totalFailed = database.totalFailed; const duration = Date.now() - startTime; const success = totalFailed === 0; return { database, summary: { totalTests, totalPassed, totalFailed, duration, success, }, }; } // Development helpers async generateTestData(projectId?: string): Promise<{ success: boolean; created: { collections: number; documents: number; users: number; files: number; }; }> { try { let collections = 0; let documents = 0; let users = 0; const files = 0; // If no project ID provided, create a test project let testProjectId = projectId; if (!testProjectId) { // SQLite-compatible: generate ID and query back (SQLite 3.35.0+ supports RETURNING, but we use compatible approach) const projectIdValue = crypto.randomUUID(); await this.db.query( "INSERT INTO projects (id, name, description, created_by) VALUES ($1, $2, $3, $4)", [projectIdValue, "Test Project", "Generated test project", "system"] ); // Query back to get the inserted row const projectResult = await this.db.query( "SELECT id FROM projects WHERE id = $1", [projectIdValue] ); testProjectId = (projectResult.rows[0] as { id: string }).id; } // Create test collections for (let i = 1; i <= 3; i++) { await this.db.query( "INSERT INTO collections (project_id, name, description, schema) VALUES ($1, $2, $3, $4)", [ testProjectId, `test_collection_${i}`, `Test collection ${i}`, JSON.stringify({ fields: [ { name: "title", type: "string", required: true }, { name: "description", type: "text", required: false }, { name: "status", type: "string", required: false }, ], }), ] ); collections++; // Create test documents for each collection for (let j = 1; j <= 5; j++) { await this.db.query( `INSERT INTO documents (collection_id, project_id, data, created_by) VALUES ((SELECT id FROM collections WHERE name = $1), $2, $3, $4)`, [ `test_collection_${i}`, testProjectId, JSON.stringify({ title: `Test Document ${j}`, description: `This is test document ${j} in collection ${i}`, status: j % 2 === 0 ? "active" : "draft", }), "system", ] ); documents++; } } // Create test users for (let i = 1; i <= 5; i++) { await this.db.query( "INSERT INTO project_users (project_id, email, username, role, is_active) VALUES ($1, $2, $3, $4, $5)", [ testProjectId, `test.user${i}@example.com`, `testuser${i}`, i === 1 ? "admin" : "member", true, ] ); users++; } return { success: true, created: { collections, documents, users, files, // Files would need file system integration }, }; } catch (error) { this.logger.error("Failed to generate test data:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "generateTestData" }); } } async cleanupTestData( projectId?: string ): Promise<{ success: boolean; deleted: number }> { try { let deleted = 0; if (projectId) { // Delete specific project's test data const result = await this.db.query( "DELETE FROM projects WHERE id = $1 AND name LIKE 'Test%'", [projectId] ); deleted = result.rowCount || 0; } else { // Delete all test data const results = await Promise.all([ this.db.query("DELETE FROM projects WHERE name LIKE 'Test%'"), this.db.query("DELETE FROM collections WHERE name LIKE 'test_%'"), this.db.query( "DELETE FROM project_users WHERE email LIKE 'test.%@example.com'" ), ]); deleted = results.reduce( (sum: number, result) => sum + (result.rowCount || 0), 0 ); } return { success: true, deleted }; } catch (error) { this.logger.error("Failed to cleanup test data:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "cleanupTestData" }); } } }