import { Component, inject, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatListModule } from '@angular/material/list'; import { MatChipsModule } from '@angular/material/chips'; import { MatCardModule } from '@angular/material/card'; import { ApiService } from '../../services/api.service'; import { AuthService } from '../../services/auth.service'; import { HttpClient } from '@angular/common/http'; import { Subject, takeUntil } from 'rxjs'; interface TestResult { name: string; status: 'PASS' | 'FAIL' | 'RUNNING' | 'SKIP'; duration_ms?: number; error?: string; details?: string; } interface TestCategory { name: string; displayName: string; tests: TestCase[]; selected: boolean; expanded: boolean; } interface TestCase { name: string; category: string; selected: boolean; testFn: () => Promise; } @Component({ selector: 'app-test', standalone: true, imports: [ CommonModule, FormsModule, MatProgressBarModule, MatCheckboxModule, MatButtonModule, MatIconModule, MatExpansionModule, MatListModule, MatChipsModule, MatCardModule ], templateUrl: './test.component.html', styleUrls: ['./test.component.scss'] }) export class TestComponent implements OnInit, OnDestroy { private apiService = inject(ApiService); private authService = inject(AuthService); private http = inject(HttpClient); private destroyed$ = new Subject(); running = false; currentTest: string = ''; testResults: TestResult[] = []; categories: TestCategory[] = [ { name: 'auth', displayName: 'Authentication Tests', tests: [], selected: true, expanded: false }, { name: 'api', displayName: 'API Endpoint Tests', tests: [], selected: true, expanded: false }, { name: 'validation', displayName: 'Validation Tests', tests: [], selected: true, expanded: false }, { name: 'integration', displayName: 'Integration Tests', tests: [], selected: true, expanded: false } ]; allSelected = false; get selectedTests(): TestCase[] { return this.categories .filter(c => c.selected) .flatMap(c => c.tests); } get totalTests(): number { return this.categories.reduce((sum, c) => sum + c.tests.length, 0); } get passedTests(): number { return this.testResults.filter(r => r.status === 'PASS').length; } get failedTests(): number { return this.testResults.filter(r => r.status === 'FAIL').length; } get progress(): number { if (this.testResults.length === 0) return 0; return (this.testResults.length / this.selectedTests.length) * 100; } ngOnInit() { this.initializeTests(); this.updateAllSelected(); } ngOnDestroy() { this.destroyed$.next(); this.destroyed$.complete(); } updateAllSelected() { this.allSelected = this.categories.length > 0 && this.categories.every(c => c.selected); } onCategorySelectionChange() { this.updateAllSelected(); } // Helper method to ensure authentication private ensureAuth(): Promise { return new Promise((resolve) => { try { // Check if we already have a valid token const token = this.authService.getToken(); if (token) { // Try to make a simple authenticated request to verify token is still valid this.apiService.getEnvironment() .pipe(takeUntil(this.destroyed$)) .subscribe({ next: () => resolve(true), error: (error: any) => { if (error.status === 401) { // Token expired, need to re-login this.authService.logout(); resolve(false); } else { // Other error, assume auth is ok resolve(true); } } }); } else { // Login with test credentials this.http.post('/api/admin/login', { username: 'admin', password: 'admin' }).pipe(takeUntil(this.destroyed$)) .subscribe({ next: (response: any) => { if (response?.token) { this.authService.setToken(response.token); this.authService.setUsername(response.username); resolve(true); } else { resolve(false); } }, error: () => resolve(false) }); } } catch { resolve(false); } }); } initializeTests() { // Authentication Tests this.addTest('auth', 'Login with valid credentials', async () => { const start = Date.now(); try { const response = await this.http.post('/api/login', { username: 'admin', password: 'admin' }).toPromise() as any; return { name: 'Login with valid credentials', status: response?.token ? 'PASS' : 'FAIL', duration_ms: Date.now() - start, details: response?.token ? 'Successfully authenticated' : 'No token received' }; } catch (error) { return { name: 'Login with valid credentials', status: 'FAIL', error: 'Login failed', duration_ms: Date.now() - start }; } }); this.addTest('auth', 'Login with invalid credentials', async () => { const start = Date.now(); try { await this.http.post('/api/login', { username: 'admin', password: 'wrong_password_12345' }).toPromise(); return { name: 'Login with invalid credentials', status: 'FAIL', error: 'Expected 401 but got success', duration_ms: Date.now() - start }; } catch (error: any) { return { name: 'Login with invalid credentials', status: error.status === 401 ? 'PASS' : 'FAIL', duration_ms: Date.now() - start, details: error.status === 401 ? 'Correctly rejected invalid credentials' : `Unexpected status: ${error.status}` }; } }); // API Endpoint Tests this.addTest('api', 'GET /api/environment', async () => { const start = Date.now(); try { if (!await this.ensureAuth()) { return { name: 'GET /api/environment', status: 'SKIP', error: 'Authentication failed', duration_ms: Date.now() - start }; } const response = await this.apiService.getEnvironment().toPromise(); return { name: 'GET /api/environment', status: response?.work_mode ? 'PASS' : 'FAIL', duration_ms: Date.now() - start, details: response?.work_mode ? `Work mode: ${response.work_mode}` : 'No work mode returned' }; } catch (error) { return { name: 'GET /api/environment', status: 'FAIL', error: 'Failed to get environment', duration_ms: Date.now() - start }; } }); this.addTest('api', 'GET /api/projects', async () => { const start = Date.now(); try { if (!await this.ensureAuth()) { return { name: 'GET /api/projects', status: 'SKIP', error: 'Authentication failed', duration_ms: Date.now() - start }; } const response = await this.apiService.getProjects().toPromise(); return { name: 'GET /api/projects', status: Array.isArray(response) ? 'PASS' : 'FAIL', duration_ms: Date.now() - start, details: Array.isArray(response) ? `Retrieved ${response.length} projects` : 'Invalid response format' }; } catch (error) { return { name: 'GET /api/projects', status: 'FAIL', error: 'Failed to get projects', duration_ms: Date.now() - start }; } }); this.addTest('api', 'GET /api/apis', async () => { const start = Date.now(); try { if (!await this.ensureAuth()) { return { name: 'GET /api/apis', status: 'SKIP', error: 'Authentication failed', duration_ms: Date.now() - start }; } const response = await this.apiService.getAPIs().toPromise(); return { name: 'GET /api/apis', status: Array.isArray(response) ? 'PASS' : 'FAIL', duration_ms: Date.now() - start, details: Array.isArray(response) ? `Retrieved ${response.length} APIs` : 'Invalid response format' }; } catch (error) { return { name: 'GET /api/apis', status: 'FAIL', error: 'Failed to get APIs', duration_ms: Date.now() - start }; } }); // Integration Tests this.addTest('integration', 'Create and delete project', async () => { const start = Date.now(); let projectId: number | undefined = undefined; try { // Ensure we're authenticated if (!await this.ensureAuth()) { return { name: 'Create and delete project', status: 'SKIP', error: 'Authentication failed', duration_ms: Date.now() - start }; } // Create test project const testProjectName = `test_project_${Date.now()}`; const createResponse = await this.apiService.createProject({ name: testProjectName, caption: 'Test Project for Integration Test', icon: 'folder', description: 'This is a test project', default_language: 'Turkish', supported_languages: ['tr'], timezone: 'Europe/Istanbul', region: 'tr-TR' }).toPromise() as any; if (!createResponse?.id) { throw new Error('Project creation failed - no ID returned'); } projectId = createResponse.id; // Verify project was created const projects = await this.apiService.getProjects().toPromise() as any[]; const createdProject = projects.find(p => p.id === projectId); if (!createdProject) { throw new Error('Created project not found in project list'); } // Delete project await this.apiService.deleteProject(projectId!).toPromise(); // Verify project was soft deleted const projectsAfterDelete = await this.apiService.getProjects().toPromise() as any[]; const deletedProject = projectsAfterDelete.find(p => p.id === projectId); if (deletedProject) { throw new Error('Project still visible after deletion'); } return { name: 'Create and delete project', status: 'PASS', duration_ms: Date.now() - start, details: `Successfully created and deleted project: ${testProjectName}` }; } catch (error: any) { // Try to clean up if project was created if (projectId !== undefined) { try { await this.apiService.deleteProject(projectId).toPromise(); } catch {} } return { name: 'Create and delete project', status: 'FAIL', error: error.message || 'Test failed', duration_ms: Date.now() - start }; } }); this.addTest('integration', 'API used in intent cannot be deleted', async () => { const start = Date.now(); let testApiName: string | undefined; let testProjectId: number | undefined; try { // Ensure we're authenticated if (!await this.ensureAuth()) { return { name: 'API used in intent cannot be deleted', status: 'SKIP', error: 'Authentication failed', duration_ms: Date.now() - start }; } // 1. Create test API testApiName = `test_api_${Date.now()}`; await this.apiService.createAPI({ name: testApiName, url: 'https://test.example.com/api', method: 'POST', timeout_seconds: 10, headers: { 'Content-Type': 'application/json' }, body_template: {}, retry: { retry_count: 3, backoff_seconds: 2, strategy: 'static' } }).toPromise(); // 2. Create test project const testProjectName = `test_project_${Date.now()}`; const createProjectResponse = await this.apiService.createProject({ name: testProjectName, caption: 'Test Project', icon: 'folder', description: 'Test project for API deletion test', default_language: 'Turkish', supported_languages: ['tr'], timezone: 'Europe/Istanbul', region: 'tr-TR' }).toPromise() as any; if (!createProjectResponse?.id) { throw new Error('Project creation failed'); } testProjectId = createProjectResponse.id; // 3. Get the first version const version = createProjectResponse.versions[0]; if (!version) { throw new Error('No version found in created project'); } // 4. Update the version to add an intent that uses our API // testProjectId is guaranteed to be a number here await this.apiService.updateVersion(testProjectId!, version.id, { caption: version.caption, general_prompt: 'Test prompt', llm: version.llm, intents: [{ name: 'test-intent', caption: 'Test Intent', locale: 'tr-TR', detection_prompt: 'Test detection', examples: ['test example'], parameters: [], action: testApiName, fallback_timeout_prompt: 'Timeout', fallback_error_prompt: 'Error' }], last_update_date: version.last_update_date }).toPromise(); // 5. Try to delete the API - this should fail with 400 try { await this.apiService.deleteAPI(testApiName).toPromise(); // If deletion succeeded, test failed return { name: 'API used in intent cannot be deleted', status: 'FAIL', error: 'API was deleted even though it was in use', duration_ms: Date.now() - start }; } catch (deleteError: any) { // Check if we got the expected 400 error const errorMessage = deleteError.error?.detail || deleteError.message || ''; const isExpectedError = deleteError.status === 400 && errorMessage.includes('API is used'); if (!isExpectedError) { console.error('Delete API Error Details:', { status: deleteError.status, error: deleteError.error, message: errorMessage }); } return { name: 'API used in intent cannot be deleted', status: isExpectedError ? 'PASS' : 'FAIL', duration_ms: Date.now() - start, details: isExpectedError ? 'Correctly prevented deletion of API in use' : `Unexpected error: Status ${deleteError.status}, Message: ${errorMessage}` }; } } catch (setupError: any) { return { name: 'API used in intent cannot be deleted', status: 'FAIL', error: `Test setup failed: ${setupError.message || setupError}`, duration_ms: Date.now() - start }; } finally { // Cleanup: first delete project, then API try { if (testProjectId !== undefined) { await this.apiService.deleteProject(testProjectId).toPromise(); } } catch {} try { if (testApiName) { await this.apiService.deleteAPI(testApiName).toPromise(); } } catch {} } }); // Validation Tests this.addTest('validation', 'Regex validation - valid pattern', async () => { const start = Date.now(); try { if (!await this.ensureAuth()) { return { name: 'Regex validation - valid pattern', status: 'SKIP', error: 'Authentication failed', duration_ms: Date.now() - start }; } const response = await this.apiService.validateRegex('^[A-Z]{3}$', 'ABC').toPromise() as any; return { name: 'Regex validation - valid pattern', status: response?.valid && response?.matches ? 'PASS' : 'FAIL', duration_ms: Date.now() - start, details: response?.valid && response?.matches ? 'Pattern matched successfully' : 'Pattern did not match or validation failed' }; } catch (error) { return { name: 'Regex validation - valid pattern', status: 'FAIL', error: 'Validation endpoint failed', duration_ms: Date.now() - start }; } }); this.addTest('validation', 'Regex validation - invalid pattern', async () => { const start = Date.now(); try { if (!await this.ensureAuth()) { return { name: 'Regex validation - invalid pattern', status: 'SKIP', error: 'Authentication failed', duration_ms: Date.now() - start }; } const response = await this.apiService.validateRegex('[invalid', 'test').toPromise() as any; return { name: 'Regex validation - invalid pattern', status: !response?.valid ? 'PASS' : 'FAIL', duration_ms: Date.now() - start, details: !response?.valid ? 'Correctly identified invalid regex' : 'Failed to identify invalid regex' }; } catch (error: any) { // Some errors are expected for invalid regex return { name: 'Regex validation - invalid pattern', status: 'PASS', duration_ms: Date.now() - start, details: 'Correctly rejected invalid regex' }; } }); // Update test counts this.categories.forEach(cat => { const originalName = cat.displayName.split(' (')[0]; cat.displayName = `${originalName} (${cat.tests.length} tests)`; }); } private addTest(category: string, name: string, testFn: () => Promise) { const cat = this.categories.find(c => c.name === category); if (cat) { cat.tests.push({ name, category, selected: true, testFn }); } } toggleAll() { this.allSelected = !this.allSelected; this.categories.forEach(c => c.selected = this.allSelected); } async runAllTests() { this.categories.forEach(c => c.selected = true); await this.runTests(); } async runSelectedTests() { await this.runTests(); } async runTests() { if (this.running || this.selectedTests.length === 0) return; this.running = true; this.testResults = []; this.currentTest = ''; try { // Ensure we're authenticated before running tests const authOk = await this.ensureAuth(); if (!authOk) { this.testResults.push({ name: 'Authentication', status: 'FAIL', error: 'Failed to authenticate for tests', duration_ms: 0 }); this.running = false; return; } // Run selected tests for (const test of this.selectedTests) { if (!this.running) break; // Allow cancellation this.currentTest = test.name; try { const result = await test.testFn(); this.testResults.push(result); } catch (error: any) { // Catch any uncaught errors from test this.testResults.push({ name: test.name, status: 'FAIL', error: error.message || 'Test threw an exception', duration_ms: 0 }); } } } catch (error: any) { console.error('Test runner error:', error); this.testResults.push({ name: 'Test Runner', status: 'FAIL', error: 'Test runner encountered an error', duration_ms: 0 }); } finally { this.running = false; this.currentTest = ''; } } stopTests() { this.running = false; this.currentTest = ''; } getTestResult(testName: string): TestResult | undefined { return this.testResults.find(r => r.name === testName); } getCategoryResults(category: TestCategory): { passed: number; failed: number; total: number } { const categoryResults = this.testResults.filter(r => category.tests.some(t => t.name === r.name) ); return { passed: categoryResults.filter(r => r.status === 'PASS').length, failed: categoryResults.filter(r => r.status === 'FAIL').length, total: category.tests.length }; } }