Spaces:
Paused
Paused
| import { Component, inject, OnInit } 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'; | |
| 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<TestResult>; | |
| } | |
| ({ | |
| 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<void>(); | |
| 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<boolean> { | |
| 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/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<TestResult>) { | |
| 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 | |
| }; | |
| } | |
| } |