328 lines
10 KiB
TypeScript
328 lines
10 KiB
TypeScript
/**
|
|
* E2E Integration Tests — Remote Diagnostics Phase 4
|
|
* Tests the complete flow: Admin creates session → Client captures → Admin views
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
const API_BASE = process.env.TEST_API_URL || 'http://localhost:4003';
|
|
const ADMIN_TOKEN = process.env.TEST_ADMIN_TOKEN || 'test-admin-token';
|
|
const PRODUCT_ID = 'test-product';
|
|
|
|
test.describe('Remote Diagnostics E2E Flow', () => {
|
|
let sessionId: string;
|
|
|
|
test('[E2E-1] Admin can create debug session', async ({ request }) => {
|
|
const response = await request.post(`${API_BASE}/api/diagnostics/sessions`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
},
|
|
data: {
|
|
targetDeviceId: 'test-device-123',
|
|
collectionLevel: 'debug',
|
|
captureLogs: true,
|
|
captureNetwork: true,
|
|
captureScreenshots: true,
|
|
maxDurationMinutes: 30,
|
|
},
|
|
});
|
|
|
|
expect(response.status()).toBe(201);
|
|
const session = await response.json();
|
|
expect(session.id).toBeDefined();
|
|
expect(session.status).toBe('pending');
|
|
expect(session.productId).toBe(PRODUCT_ID);
|
|
|
|
sessionId = session.id;
|
|
});
|
|
|
|
test('[E2E-2] Client can poll for active session config', async ({ request }) => {
|
|
// First, admin must activate the session
|
|
const activateResponse = await request.patch(`${API_BASE}/api/diagnostics/sessions/${sessionId}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
},
|
|
data: {
|
|
status: 'active',
|
|
},
|
|
});
|
|
expect(activateResponse.status()).toBe(200);
|
|
|
|
// Now client polls for config
|
|
const configResponse = await request.get(`${API_BASE}/api/diagnostics/config`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
'X-Device-Id': 'test-device-123',
|
|
},
|
|
});
|
|
|
|
expect(configResponse.status()).toBe(200);
|
|
const config = await configResponse.json();
|
|
expect(config.enabled).toBe(true);
|
|
expect(config.sessionId).toBe(sessionId);
|
|
expect(config.captureLogs).toBe(true);
|
|
expect(config.captureNetwork).toBe(true);
|
|
});
|
|
|
|
test('[E2E-3] Client can ingest trace spans', async ({ request }) => {
|
|
const response = await request.post(`${API_BASE}/api/diagnostics/sessions/${sessionId}/traces`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
'X-Device-Id': 'test-device-123',
|
|
},
|
|
data: {
|
|
sessionId,
|
|
traces: [
|
|
{
|
|
traceId: 'trace-001',
|
|
spanId: 'span-001',
|
|
name: 'UserLogin',
|
|
startTime: new Date().toISOString(),
|
|
durationMs: 150,
|
|
attributes: { userId: 'user-123' },
|
|
status: 'ok',
|
|
},
|
|
{
|
|
traceId: 'trace-002',
|
|
spanId: 'span-002',
|
|
parentId: 'span-001',
|
|
name: 'DatabaseQuery',
|
|
startTime: new Date().toISOString(),
|
|
durationMs: 50,
|
|
attributes: { query: 'SELECT * FROM users' },
|
|
status: 'ok',
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(response.status()).toBe(200);
|
|
const result = await response.json();
|
|
expect(result.accepted).toBe(2);
|
|
});
|
|
|
|
test('[E2E-4] Client can ingest log entries', async ({ request }) => {
|
|
const response = await request.post(`${API_BASE}/api/diagnostics/sessions/${sessionId}/logs`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
'X-Device-Id': 'test-device-123',
|
|
},
|
|
data: {
|
|
sessionId,
|
|
logs: [
|
|
{
|
|
level: 'info',
|
|
message: 'Application started successfully',
|
|
timestamp: new Date().toISOString(),
|
|
module: 'AppLifecycle',
|
|
context: { version: '1.0.0' },
|
|
},
|
|
{
|
|
level: 'debug',
|
|
message: 'User authenticated',
|
|
timestamp: new Date().toISOString(),
|
|
module: 'AuthManager',
|
|
context: { userId: 'user-123', method: 'token' },
|
|
},
|
|
{
|
|
level: 'error',
|
|
message: 'Failed to load configuration',
|
|
timestamp: new Date().toISOString(),
|
|
module: 'ConfigLoader',
|
|
context: { error: 'File not found' },
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(response.status()).toBe(200);
|
|
const result = await response.json();
|
|
expect(result.accepted).toBe(3);
|
|
});
|
|
|
|
test('[E2E-5] Admin can query traces for session', async ({ request }) => {
|
|
const response = await request.get(`${API_BASE}/api/diagnostics/sessions/${sessionId}/traces`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
},
|
|
});
|
|
|
|
expect(response.status()).toBe(200);
|
|
const result = await response.json();
|
|
expect(Array.isArray(result.traces)).toBe(true);
|
|
expect(result.traces.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
test('[E2E-6] Admin can query logs for session', async ({ request }) => {
|
|
const response = await request.get(`${API_BASE}/api/diagnostics/sessions/${sessionId}/logs`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
},
|
|
});
|
|
|
|
expect(response.status()).toBe(200);
|
|
const result = await response.json();
|
|
expect(Array.isArray(result.logs)).toBe(true);
|
|
expect(result.logs.length).toBeGreaterThanOrEqual(3);
|
|
|
|
// Verify log levels are preserved
|
|
const errorLog = result.logs.find((l: { level: string }) => l.level === 'error');
|
|
expect(errorLog).toBeDefined();
|
|
expect(errorLog.message).toContain('Failed to load');
|
|
});
|
|
|
|
test('[E2E-7] Admin can complete debug session', async ({ request }) => {
|
|
const response = await request.patch(`${API_BASE}/api/diagnostics/sessions/${sessionId}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
},
|
|
data: {
|
|
status: 'completed',
|
|
},
|
|
});
|
|
|
|
expect(response.status()).toBe(200);
|
|
const session = await response.json();
|
|
expect(session.status).toBe('completed');
|
|
expect(session.endedAt).toBeDefined();
|
|
expect(session.traceCount).toBeGreaterThanOrEqual(2);
|
|
expect(session.logCount).toBeGreaterThanOrEqual(3);
|
|
});
|
|
});
|
|
|
|
test.describe('Auto-Trigger E2E Tests', () => {
|
|
test('[E2E-AUTO-1] Admin can create error-threshold trigger', async ({ request }) => {
|
|
const response = await request.post(`${API_BASE}/api/diagnostics/triggers`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
},
|
|
data: {
|
|
productId: PRODUCT_ID,
|
|
name: 'High Error Rate Alert',
|
|
enabled: true,
|
|
condition: {
|
|
type: 'error_rate',
|
|
threshold: 0.1, // 10%
|
|
windowMinutes: 5,
|
|
minEvents: 50,
|
|
},
|
|
sessionConfig: {
|
|
collectionLevel: 'debug',
|
|
captureLogs: true,
|
|
captureNetwork: true,
|
|
captureScreenshots: true,
|
|
maxDurationMinutes: 60,
|
|
},
|
|
notifications: {
|
|
emailAdmins: true,
|
|
},
|
|
cooldownMinutes: 30,
|
|
},
|
|
});
|
|
|
|
expect(response.status()).toBe(201);
|
|
const result = await response.json();
|
|
expect(result.trigger.id).toBeDefined();
|
|
expect(result.trigger.condition.type).toBe('error_rate');
|
|
});
|
|
|
|
test('[E2E-AUTO-2] Admin can manually run triggers', async ({ request }) => {
|
|
const response = await request.post(`${API_BASE}/api/diagnostics/triggers/run`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
},
|
|
data: {
|
|
productId: PRODUCT_ID,
|
|
},
|
|
});
|
|
|
|
expect(response.status()).toBe(200);
|
|
const result = await response.json();
|
|
expect(Array.isArray(result.results)).toBe(true);
|
|
});
|
|
|
|
test('[E2E-CRASH-1] Client can report crash and trigger auto-session', async ({ request }) => {
|
|
const response = await request.post(`${API_BASE}/api/diagnostics/crash-report`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
},
|
|
data: {
|
|
productId: PRODUCT_ID,
|
|
deviceId: 'test-crash-device',
|
|
sessionId: 'test-session-456',
|
|
timestamp: new Date().toISOString(),
|
|
errorType: 'SIGSEGV',
|
|
errorMessage: 'Segmentation fault at 0x00000000',
|
|
stackTrace: 'Thread 0: 0x001 0x002 0x003',
|
|
breadcrumbs: [
|
|
{ timestamp: new Date().toISOString(), category: 'navigation', message: 'Opened settings' },
|
|
{ timestamp: new Date().toISOString(), category: 'action', message: 'Clicked save' },
|
|
],
|
|
deviceState: {
|
|
memoryUsage: 1024 * 1024 * 100, // 100MB
|
|
batteryLevel: 85,
|
|
osVersion: 'iOS 17.2',
|
|
appVersion: '1.5.0',
|
|
},
|
|
},
|
|
});
|
|
|
|
// Should accept the crash report (session may or may not be created due to cooldown)
|
|
expect([201, 202]).toContain(response.status());
|
|
const result = await response.json();
|
|
expect(result.accepted).toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe('Access Control E2E Tests', () => {
|
|
test('[E2E-AC-1] Non-admin cannot create session', async ({ request }) => {
|
|
const response = await request.post(`${API_BASE}/api/diagnostics/sessions`, {
|
|
headers: {
|
|
'Authorization': 'Bearer invalid-token',
|
|
'X-Product-Id': PRODUCT_ID,
|
|
},
|
|
data: {
|
|
targetDeviceId: 'test-device',
|
|
},
|
|
});
|
|
|
|
expect(response.status()).toBe(401);
|
|
});
|
|
|
|
test('[E2E-AC-2] User cannot access another user\'s session data', async ({ request }) => {
|
|
// Create a session for user A
|
|
const createResponse = await request.post(`${API_BASE}/api/diagnostics/sessions`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
|
'X-Product-Id': PRODUCT_ID,
|
|
},
|
|
data: {
|
|
targetUserId: 'user-a',
|
|
collectionLevel: 'standard',
|
|
},
|
|
});
|
|
const session = await createResponse.json();
|
|
|
|
// User B tries to access
|
|
const getResponse = await request.get(`${API_BASE}/api/diagnostics/sessions/${session.id}`, {
|
|
headers: {
|
|
'Authorization': 'Bearer user-b-token',
|
|
'X-Product-Id': PRODUCT_ID,
|
|
},
|
|
});
|
|
|
|
expect(getResponse.status()).toBe(403);
|
|
});
|
|
});
|