flare / flare-ui /src /app /components /test /test.component.ts
ciyidogan's picture
Upload 118 files
9f79da5 verified
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>;
}
@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<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
};
}
}