Spaces:
Building
Building
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<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/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<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 | |
}; | |
} | |
} |