265 lines
9.5 KiB
TypeScript
265 lines
9.5 KiB
TypeScript
import React from 'react';
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { OperationsInfrastructure } from '../components/ops/operations-infrastructure';
|
|
|
|
describe('OperationsInfrastructure Speedtest Options Component Tests', () => {
|
|
const originalFetch = global.fetch;
|
|
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
});
|
|
|
|
const mockInfraResponse = {
|
|
resources: {
|
|
cpu_percent: 12.0,
|
|
load_avg: [0.1, 0.2, 0.15],
|
|
memory: { total: 16000000000, used: 4000000000, available: 12000000000, percent: 25.0 },
|
|
swap: { total: 4000000000, used: 0, percent: 0.0 },
|
|
disk: { total: 100000000000, used: 20000000000, available: 80000000000, percent: 20.0 },
|
|
uptime_seconds: 50000,
|
|
uptime_formatted: '13h 53m',
|
|
},
|
|
services: [
|
|
{ name: 'motorsignal-website', status: 'active' },
|
|
{ name: 'motorsignal-api', status: 'active' },
|
|
{ name: 'nginx', status: 'active' },
|
|
{ name: 'postgresql', status: 'active' },
|
|
],
|
|
network: {
|
|
bytes_received: 1048576,
|
|
bytes_transmitted: 2097152,
|
|
label: 'Cumulative Total Since Boot',
|
|
},
|
|
deployment: {
|
|
git_revision: 'abcdef1234567890abcdef1234567890abcdef12',
|
|
short_revision: 'abcdef1',
|
|
revision_status: 'known',
|
|
website_health: 'available',
|
|
api_health: 'available',
|
|
},
|
|
};
|
|
|
|
it('renders both speedtest option tabs with intuitive labels', async () => {
|
|
global.fetch = vi.fn().mockImplementation((url: string) => {
|
|
if (url.includes('/api/ops/infrastructure') && !url.includes('/speedtest')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockInfraResponse,
|
|
});
|
|
}
|
|
return Promise.reject(new Error('Unknown URL'));
|
|
});
|
|
|
|
render(<OperationsInfrastructure role="operator" />);
|
|
|
|
// Wait for initial data load
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Network Cumulative Totals')).toBeDefined();
|
|
});
|
|
|
|
// Check option 1 tab: My Connection -> MotorSignal
|
|
const clientTab = screen.getByRole('button', { name: /My Connection → MotorSignal/i });
|
|
expect(clientTab).toBeDefined();
|
|
|
|
// Check option 2 tab: MotorSignal -> Upstream Server
|
|
const serverTab = screen.getByRole('button', { name: /MotorSignal → Upstream Server/i });
|
|
expect(serverTab).toBeDefined();
|
|
|
|
// Check action button reflects client mode initially
|
|
expect(screen.getByRole('button', { name: /Run Client Speedtest/i })).toBeDefined();
|
|
|
|
// Check description mentions client to server
|
|
expect(screen.getByText(/Measures latency, download, and upload between your operator browser/i)).toBeDefined();
|
|
});
|
|
|
|
it('switches between Client to Server and Server to External tabs', async () => {
|
|
global.fetch = vi.fn().mockImplementation((url: string) => {
|
|
if (url.includes('/api/ops/infrastructure') && !url.includes('/speedtest')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockInfraResponse,
|
|
});
|
|
}
|
|
return Promise.reject(new Error('Unknown URL'));
|
|
});
|
|
|
|
render(<OperationsInfrastructure role="operator" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Network Cumulative Totals')).toBeDefined();
|
|
});
|
|
|
|
// Click Server to External tab
|
|
const serverTab = screen.getByRole('button', { name: /MotorSignal → Upstream Server/i });
|
|
fireEvent.click(serverTab);
|
|
|
|
// Check button updates to server speedtest
|
|
expect(screen.getByRole('button', { name: /Run Server Speedtest/i })).toBeDefined();
|
|
|
|
// Check description mentions upstream Cloudflare edge
|
|
expect(screen.getByText(/Measures server-side network performance from MotorSignal production host to upstream/i)).toBeDefined();
|
|
|
|
// Click back to Client to Server tab
|
|
const clientTab = screen.getByRole('button', { name: /My Connection → MotorSignal/i });
|
|
fireEvent.click(clientTab);
|
|
|
|
expect(screen.getByRole('button', { name: /Run Client Speedtest/i })).toBeDefined();
|
|
});
|
|
|
|
it('runs Server to External upstream speedtest and displays results with directional labels', async () => {
|
|
global.fetch = vi.fn().mockImplementation((url: string, opts?: RequestInit) => {
|
|
if (url.includes('/api/ops/infrastructure/speedtest')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({
|
|
status: 'completed',
|
|
tested_at: new Date().toISOString(),
|
|
latency_ms: 12.4,
|
|
download_mbps: 345.67,
|
|
upload_mbps: 189.23,
|
|
server_target: 'Cloudflare Edge (1.1.1.1)',
|
|
client_ip: '198.51.100.24',
|
|
mode: 'server_to_external',
|
|
direction_label: 'MotorSignal Server ➔ Upstream Edge (1.1.1.1)',
|
|
}),
|
|
});
|
|
}
|
|
if (url.includes('/api/ops/infrastructure')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockInfraResponse,
|
|
});
|
|
}
|
|
return Promise.reject(new Error('Unknown URL'));
|
|
});
|
|
|
|
render(<OperationsInfrastructure role="operator" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Network Cumulative Totals')).toBeDefined();
|
|
});
|
|
|
|
// Switch to Server to External
|
|
fireEvent.click(screen.getByRole('button', { name: /MotorSignal → Upstream Server/i }));
|
|
|
|
const runBtn = screen.getByRole('button', { name: /Run Server Speedtest/i });
|
|
fireEvent.click(runBtn);
|
|
|
|
// Verify results render
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Server to External Result (MotorSignal → Upstream Server)')).toBeDefined();
|
|
expect(screen.getByText('12.4 ms')).toBeDefined();
|
|
expect(screen.getByText('345.67 Mbps')).toBeDefined();
|
|
expect(screen.getByText('189.23 Mbps')).toBeDefined();
|
|
expect(screen.getByText(/Target: Cloudflare Edge \(1.1.1.1\)/i)).toBeDefined();
|
|
expect(screen.getByText(/IP: 198.51.100.24/i)).toBeDefined();
|
|
});
|
|
});
|
|
|
|
it('runs Client to Server speedtest with ping, download, and upload phases', async () => {
|
|
global.fetch = vi.fn().mockImplementation((url: string, opts?: RequestInit) => {
|
|
if (url.includes('/api/ops/infrastructure/speedtest')) {
|
|
if (url.includes('action=ping')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ status: 'ok', client_ip: '203.0.113.88' }),
|
|
});
|
|
}
|
|
if (url.includes('action=download')) {
|
|
const testBuffer = new Uint8Array(2500000);
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
headers: new Headers({ 'X-Client-IP': '203.0.113.88' }),
|
|
arrayBuffer: async () => testBuffer.buffer,
|
|
blob: async () => new Blob([testBuffer]),
|
|
});
|
|
}
|
|
if (url.includes('action=upload')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ status: 'completed', received_bytes: 1500000, client_ip: '203.0.113.88' }),
|
|
});
|
|
}
|
|
if (url.includes('action=record_client_result')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ status: 'completed' }),
|
|
});
|
|
}
|
|
}
|
|
if (url.includes('/api/ops/infrastructure')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockInfraResponse,
|
|
});
|
|
}
|
|
return Promise.reject(new Error('Unknown URL'));
|
|
});
|
|
|
|
render(<OperationsInfrastructure role="operator" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Network Cumulative Totals')).toBeDefined();
|
|
});
|
|
|
|
const runBtn = screen.getByRole('button', { name: /Run Client Speedtest/i });
|
|
fireEvent.click(runBtn);
|
|
|
|
// Verify results render
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Client to Server Result (My Connection → MotorSignal)')).toBeDefined();
|
|
expect(screen.getByText(/Target: MotorSignal Host Edge/i)).toBeDefined();
|
|
expect(screen.getByText(/IP: 203.0.113.88/i)).toBeDefined();
|
|
expect(screen.getByText(/Route: Operator Browser ➔ MotorSignal Server/i)).toBeDefined();
|
|
});
|
|
});
|
|
|
|
it('displays error banner gracefully when speedtest fails', async () => {
|
|
global.fetch = vi.fn().mockImplementation((url: string) => {
|
|
if (url.includes('/api/ops/infrastructure/speedtest')) {
|
|
return Promise.resolve({
|
|
ok: false,
|
|
status: 503,
|
|
json: async () => ({ detail: 'Operations speedtest service temporarily unavailable.' }),
|
|
});
|
|
}
|
|
if (url.includes('/api/ops/infrastructure')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockInfraResponse,
|
|
});
|
|
}
|
|
return Promise.reject(new Error('Unknown URL'));
|
|
});
|
|
|
|
render(<OperationsInfrastructure role="operator" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Network Cumulative Totals')).toBeDefined();
|
|
});
|
|
|
|
// Run in server mode to test error
|
|
fireEvent.click(screen.getByRole('button', { name: /MotorSignal → Upstream Server/i }));
|
|
fireEvent.click(screen.getByRole('button', { name: /Run Server Speedtest/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Operations speedtest service temporarily unavailable/i)).toBeDefined();
|
|
});
|
|
});
|
|
});
|