edb713547f
Phase 5 adds comprehensive E2E test coverage for backup management, log viewing, import wizards, and uptime monitoring features. Backend Changes: Add POST /api/v1/uptime/monitors endpoint for creating monitors Add CreateMonitor service method with URL validation Add 9 unit tests for uptime handler create functionality Frontend Changes: Add CreateMonitorModal component to Uptime.tsx Add "Add Monitor" and "Sync with Hosts" buttons Add createMonitor() API function to uptime.ts Add data-testid attributes to 6 frontend components: Backups.tsx, Uptime.tsx, LiveLogViewer.tsx Logs.tsx, ImportCaddy.tsx, ImportCrowdSec.tsx E2E Test Files Created (7 files, ~115 tests): backups-create.spec.ts (17 tests) backups-restore.spec.ts (8 tests) logs-viewing.spec.ts (20 tests) import-caddyfile.spec.ts (20 tests) import-crowdsec.spec.ts (8 tests) uptime-monitoring.spec.ts (22 tests) real-time-logs.spec.ts (20 tests) Coverage: Backend 87.0%, Frontend 85.2%
332 lines
11 KiB
TypeScript
332 lines
11 KiB
TypeScript
/**
|
|
* Import CrowdSec Configuration - E2E Tests
|
|
*
|
|
* Tests for the CrowdSec configuration import functionality.
|
|
* Covers 8 test scenarios as defined in phase5-implementation.md.
|
|
*
|
|
* Test Categories:
|
|
* - Page Layout (2 tests): heading, form display
|
|
* - File Validation (3 tests): valid file, invalid format, missing fields
|
|
* - Import Execution (3 tests): import success, error handling, already exists
|
|
*/
|
|
|
|
import { test, expect, loginUser } from '../fixtures/auth-fixtures';
|
|
import { waitForToast, waitForLoadingComplete, waitForAPIResponse } from '../utils/wait-helpers';
|
|
|
|
/**
|
|
* Selectors for the Import CrowdSec page
|
|
*/
|
|
const SELECTORS = {
|
|
// Page elements
|
|
pageTitle: 'h1',
|
|
fileInput: '[data-testid="crowdsec-import-file"]',
|
|
progress: '[data-testid="import-progress"]',
|
|
|
|
// Buttons
|
|
importButton: 'button:has-text("Import")',
|
|
|
|
// Error/success messages
|
|
errorMessage: '.bg-red-900',
|
|
successToast: '[data-testid="toast-success"]',
|
|
};
|
|
|
|
/**
|
|
* Mock CrowdSec configuration for testing
|
|
*/
|
|
const mockCrowdSecConfig = {
|
|
lapi_url: 'http://crowdsec:8080',
|
|
bouncer_api_key: 'test-api-key',
|
|
mode: 'live',
|
|
};
|
|
|
|
/**
|
|
* Helper to create a mock tar.gz file buffer
|
|
*/
|
|
function createMockTarGzBuffer(): Buffer {
|
|
return Buffer.from('mock tar.gz content for crowdsec config');
|
|
}
|
|
|
|
/**
|
|
* Helper to create a mock zip file buffer
|
|
*/
|
|
function createMockZipBuffer(): Buffer {
|
|
return Buffer.from('mock zip content for crowdsec config');
|
|
}
|
|
|
|
test.describe('Import CrowdSec Configuration', () => {
|
|
// =========================================================================
|
|
// Page Layout Tests (2 tests)
|
|
// =========================================================================
|
|
test.describe('Page Layout', () => {
|
|
test('should display import page with correct heading', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
await page.goto('/tasks/import/crowdsec');
|
|
await waitForLoadingComplete(page);
|
|
|
|
await expect(page.locator(SELECTORS.pageTitle)).toContainText(/crowdsec|import/i);
|
|
});
|
|
|
|
test('should show file upload form with accepted formats', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
await page.goto('/tasks/import/crowdsec');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Verify file input is visible
|
|
const fileInput = page.locator(SELECTORS.fileInput);
|
|
await expect(fileInput).toBeVisible();
|
|
|
|
// Verify it accepts proper file types (.tar.gz, .zip)
|
|
await expect(fileInput).toHaveAttribute('accept', /\.tar\.gz|\.zip/);
|
|
|
|
// Verify import button exists
|
|
const importButton = page.locator(SELECTORS.importButton);
|
|
await expect(importButton).toBeVisible();
|
|
|
|
// Verify progress section exists
|
|
await expect(page.locator(SELECTORS.progress)).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// File Validation Tests (3 tests)
|
|
// =========================================================================
|
|
test.describe('File Validation', () => {
|
|
test('should accept valid .tar.gz configuration files', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
// Mock backup and import APIs
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
await route.fulfill({
|
|
status: 201,
|
|
json: { filename: 'pre-import-backup.tar.gz', size: 1000, time: new Date().toISOString() },
|
|
});
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
await page.route('**/api/v1/crowdsec/import', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: { message: 'Import successful' },
|
|
});
|
|
});
|
|
|
|
await page.goto('/tasks/import/crowdsec');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Upload .tar.gz file
|
|
const fileInput = page.locator(SELECTORS.fileInput);
|
|
await fileInput.setInputFiles({
|
|
name: 'crowdsec-config.tar.gz',
|
|
mimeType: 'application/gzip',
|
|
buffer: createMockTarGzBuffer(),
|
|
});
|
|
|
|
// Verify file was accepted (import button should be enabled)
|
|
await expect(page.locator(SELECTORS.importButton)).toBeEnabled();
|
|
});
|
|
|
|
test('should accept valid .zip configuration files', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
// Mock backup and import APIs
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
await route.fulfill({
|
|
status: 201,
|
|
json: { filename: 'pre-import-backup.tar.gz', size: 1000, time: new Date().toISOString() },
|
|
});
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
await page.route('**/api/v1/crowdsec/import', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: { message: 'Import successful' },
|
|
});
|
|
});
|
|
|
|
await page.goto('/tasks/import/crowdsec');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Upload .zip file
|
|
const fileInput = page.locator(SELECTORS.fileInput);
|
|
await fileInput.setInputFiles({
|
|
name: 'crowdsec-config.zip',
|
|
mimeType: 'application/zip',
|
|
buffer: createMockZipBuffer(),
|
|
});
|
|
|
|
// Verify file was accepted (import button should be enabled)
|
|
await expect(page.locator(SELECTORS.importButton)).toBeEnabled();
|
|
});
|
|
|
|
test('should disable import button when no file selected', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
await page.goto('/tasks/import/crowdsec');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Import button should be disabled when no file is selected
|
|
await expect(page.locator(SELECTORS.importButton)).toBeDisabled();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Import Execution Tests (3 tests)
|
|
// =========================================================================
|
|
test.describe('Import Execution', () => {
|
|
test('should create backup before import and complete successfully', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
let backupCalled = false;
|
|
let importCalled = false;
|
|
const callOrder: string[] = [];
|
|
|
|
// Mock backup API
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
backupCalled = true;
|
|
callOrder.push('backup');
|
|
await route.fulfill({
|
|
status: 201,
|
|
json: { filename: 'pre-import-backup.tar.gz', size: 1000, time: new Date().toISOString() },
|
|
});
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
// Mock import API
|
|
await page.route('**/api/v1/crowdsec/import', async (route) => {
|
|
importCalled = true;
|
|
callOrder.push('import');
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: { message: 'CrowdSec configuration imported successfully' },
|
|
});
|
|
});
|
|
|
|
await page.goto('/tasks/import/crowdsec');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Upload file
|
|
const fileInput = page.locator(SELECTORS.fileInput);
|
|
await fileInput.setInputFiles({
|
|
name: 'crowdsec-config.tar.gz',
|
|
mimeType: 'application/gzip',
|
|
buffer: createMockTarGzBuffer(),
|
|
});
|
|
|
|
// Click import button
|
|
await page.locator(SELECTORS.importButton).click();
|
|
|
|
// Wait for both API calls
|
|
await waitForAPIResponse(page, '/api/v1/crowdsec/import', { status: 200 });
|
|
|
|
// Verify backup was called FIRST, then import
|
|
expect(backupCalled).toBe(true);
|
|
expect(importCalled).toBe(true);
|
|
expect(callOrder).toEqual(['backup', 'import']);
|
|
|
|
// Verify success toast
|
|
await waitForToast(page, /success|imported/i);
|
|
});
|
|
|
|
test('should handle import errors gracefully', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
// Mock backup API (success)
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
await route.fulfill({
|
|
status: 201,
|
|
json: { filename: 'pre-import-backup.tar.gz', size: 1000, time: new Date().toISOString() },
|
|
});
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
// Mock import API (failure)
|
|
await page.route('**/api/v1/crowdsec/import', async (route) => {
|
|
await route.fulfill({
|
|
status: 400,
|
|
json: { error: 'Invalid configuration format: missing required field "lapi_url"' },
|
|
});
|
|
});
|
|
|
|
await page.goto('/tasks/import/crowdsec');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Upload file
|
|
const fileInput = page.locator(SELECTORS.fileInput);
|
|
await fileInput.setInputFiles({
|
|
name: 'crowdsec-config.tar.gz',
|
|
mimeType: 'application/gzip',
|
|
buffer: createMockTarGzBuffer(),
|
|
});
|
|
|
|
// Click import button
|
|
await page.locator(SELECTORS.importButton).click();
|
|
|
|
// Wait for import API call
|
|
await waitForAPIResponse(page, '/api/v1/crowdsec/import', { status: 400 });
|
|
|
|
// Verify error toast
|
|
await waitForToast(page, /error|failed|invalid/i);
|
|
});
|
|
|
|
test('should show loading state during import', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
// Mock backup API with delay
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
await route.fulfill({
|
|
status: 201,
|
|
json: { filename: 'pre-import-backup.tar.gz', size: 1000, time: new Date().toISOString() },
|
|
});
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
// Mock import API with delay
|
|
await page.route('**/api/v1/crowdsec/import', async (route) => {
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: { message: 'Import successful' },
|
|
});
|
|
});
|
|
|
|
await page.goto('/tasks/import/crowdsec');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Upload file
|
|
const fileInput = page.locator(SELECTORS.fileInput);
|
|
await fileInput.setInputFiles({
|
|
name: 'crowdsec-config.tar.gz',
|
|
mimeType: 'application/gzip',
|
|
buffer: createMockTarGzBuffer(),
|
|
});
|
|
|
|
// Click import button
|
|
const importButton = page.locator(SELECTORS.importButton);
|
|
await importButton.click();
|
|
|
|
// Button should be disabled during import (loading state)
|
|
await expect(importButton).toBeDisabled();
|
|
|
|
// Wait for import to complete
|
|
await waitForAPIResponse(page, '/api/v1/crowdsec/import', { status: 200 });
|
|
|
|
// Button should be enabled again after completion
|
|
await expect(importButton).toBeEnabled();
|
|
});
|
|
});
|
|
});
|