Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
395 lines
14 KiB
TypeScript
Executable File
395 lines
14 KiB
TypeScript
Executable File
/**
|
|
* Backups Page - Restore E2E Tests
|
|
*
|
|
* Tests for backup restoration functionality including confirmation dialog,
|
|
* restore execution, progress tracking, and error handling.
|
|
* Covers 8 test scenarios as defined in phase5-implementation.md.
|
|
*
|
|
* Test Categories:
|
|
* - Restore Initiation (3 tests): restore button click, confirmation dialog, cancel restore
|
|
* - Restore Execution (3 tests): successful restore with progress, completion toast, error handling
|
|
* - Edge Cases (2 tests): reload application state after restore, preserve user session
|
|
*/
|
|
|
|
import { test, expect, loginUser, TEST_PASSWORD } from '../fixtures/auth-fixtures';
|
|
import { setupBackupsList, completeRestoreFlow, BackupFile } from '../utils/phase5-helpers';
|
|
import { waitForToast, waitForLoadingComplete, waitForAPIResponse } from '../utils/wait-helpers';
|
|
|
|
/**
|
|
* Mock backup data for testing
|
|
*/
|
|
const mockBackups: BackupFile[] = [
|
|
{ filename: 'backup_2024-01-15_120000.tar.gz', size: 1048576, time: '2024-01-15T12:00:00Z' },
|
|
{ filename: 'backup_2024-01-14_120000.tar.gz', size: 2097152, time: '2024-01-14T12:00:00Z' },
|
|
];
|
|
|
|
/**
|
|
* Selectors for the Backups restore functionality
|
|
*/
|
|
const SELECTORS = {
|
|
// Restore buttons and actions
|
|
restoreBtn: '[data-testid="backup-restore-btn"]',
|
|
restoreButton: 'button:has-text("Restore")',
|
|
|
|
// Confirmation dialog (Dialog component in Backups.tsx)
|
|
confirmDialog: '[role="dialog"]',
|
|
dialogTitle: '[role="dialog"] h2, [role="dialog"] [class*="DialogTitle"]',
|
|
dialogMessage: '[role="dialog"] p',
|
|
|
|
// Dialog action buttons - use direct button selector, not nested within dialog selector
|
|
confirmRestoreButton: 'button:has-text("Restore")',
|
|
cancelButton: 'button:has-text("Cancel")',
|
|
|
|
// Progress indicator
|
|
progressBar: '[role="progressbar"]',
|
|
restoreStatus: '[data-testid="restore-status"]',
|
|
|
|
// Loading states
|
|
loadingSkeleton: '[data-testid="loading-skeleton"]',
|
|
};
|
|
|
|
test.describe('Backups Page - Restore', () => {
|
|
// =========================================================================
|
|
// Restore Initiation Tests (3 tests)
|
|
// =========================================================================
|
|
test.describe('Restore Initiation', () => {
|
|
test('should show confirmation dialog when clicking restore button', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ status: 200, json: mockBackups });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.goto('/tasks/backups');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Click restore on first backup
|
|
const restoreButton = page.locator(SELECTORS.restoreBtn).first();
|
|
await restoreButton.click();
|
|
|
|
// Verify dialog appears
|
|
const dialog = page.locator(SELECTORS.confirmDialog);
|
|
await expect(dialog).toBeVisible();
|
|
|
|
// Verify dialog contains restore-related content
|
|
await expect(dialog).toContainText(/restore/i);
|
|
});
|
|
|
|
test('should display warning message about data replacement in restore dialog', async ({
|
|
page,
|
|
adminUser,
|
|
}) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ status: 200, json: mockBackups });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.goto('/tasks/backups');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Click restore on first backup
|
|
const restoreButton = page.locator(SELECTORS.restoreBtn).first();
|
|
await restoreButton.click();
|
|
|
|
// Verify dialog shows warning message (from translation key backups.restoreConfirmMessage)
|
|
const dialog = page.locator(SELECTORS.confirmDialog);
|
|
await expect(dialog).toBeVisible();
|
|
|
|
// The dialog should contain a message about the restore action
|
|
const dialogMessage = dialog.locator('p');
|
|
await expect(dialogMessage).toBeVisible();
|
|
});
|
|
|
|
test('should cancel restore when clicking cancel button', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
let restoreRequested = false;
|
|
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ status: 200, json: mockBackups });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.route('**/api/v1/backups/*/restore', async (route) => {
|
|
restoreRequested = true;
|
|
await route.fulfill({ status: 200, json: { message: 'Restore completed' } });
|
|
});
|
|
|
|
await page.goto('/tasks/backups');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Click restore on first backup
|
|
const restoreButton = page.locator(SELECTORS.restoreBtn).first();
|
|
await restoreButton.click();
|
|
|
|
// Wait for dialog
|
|
const dialog = page.locator(SELECTORS.confirmDialog);
|
|
await expect(dialog).toBeVisible();
|
|
|
|
// Click cancel button
|
|
const cancelButton = dialog.locator(SELECTORS.cancelButton);
|
|
await cancelButton.click();
|
|
|
|
// Dialog should close
|
|
await expect(dialog).not.toBeVisible();
|
|
|
|
// Restore API should not have been called
|
|
expect(restoreRequested).toBe(false);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Restore Execution Tests (3 tests)
|
|
// =========================================================================
|
|
test.describe('Restore Execution', () => {
|
|
test('should restore backup successfully after confirmation', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
const filename = 'backup_2024-01-15_120000.tar.gz';
|
|
let restoreRequested = false;
|
|
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ status: 200, json: mockBackups });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.route(`**/api/v1/backups/${filename}/restore`, async (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
restoreRequested = true;
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: { message: 'Restore completed successfully' },
|
|
});
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.goto('/tasks/backups');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Click restore on first backup
|
|
const restoreButton = page.locator(SELECTORS.restoreBtn).first();
|
|
await restoreButton.click();
|
|
|
|
// Wait for dialog
|
|
const dialog = page.locator(SELECTORS.confirmDialog);
|
|
await expect(dialog).toBeVisible();
|
|
|
|
// Click confirm restore button
|
|
const confirmButton = dialog.locator(SELECTORS.confirmRestoreButton);
|
|
await confirmButton.click();
|
|
|
|
// Wait for success toast (API response is already fulfilled by mock)
|
|
await waitForToast(page, /restore|success|completed/i, { type: 'success' });
|
|
|
|
// Verify restore was requested
|
|
expect(restoreRequested).toBe(true);
|
|
|
|
// Dialog should close after successful restore
|
|
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('should show success toast after successful restoration', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
const filename = 'backup_2024-01-15_120000.tar.gz';
|
|
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ status: 200, json: mockBackups });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.route(`**/api/v1/backups/${filename}/restore`, async (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: { message: 'Restore completed successfully' },
|
|
});
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.goto('/tasks/backups');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Click restore on first backup
|
|
const restoreButton = page.locator(SELECTORS.restoreBtn).first();
|
|
await restoreButton.click();
|
|
|
|
// Wait for dialog and confirm
|
|
const dialog = page.locator(SELECTORS.confirmDialog);
|
|
await expect(dialog).toBeVisible();
|
|
|
|
const confirmButton = dialog.locator(SELECTORS.confirmRestoreButton);
|
|
await confirmButton.click();
|
|
|
|
// Wait for success toast
|
|
await waitForToast(page, /success|restored|completed/i, { type: 'success' });
|
|
});
|
|
|
|
test('should handle restore failure gracefully with error toast', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
const filename = 'backup_2024-01-15_120000.tar.gz';
|
|
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ status: 200, json: mockBackups });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.route(`**/api/v1/backups/${filename}/restore`, async (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
await route.fulfill({
|
|
status: 500,
|
|
json: { error: 'Internal server error: backup file corrupted' },
|
|
});
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.goto('/tasks/backups');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Click restore on first backup
|
|
const restoreButton = page.locator(SELECTORS.restoreBtn).first();
|
|
await restoreButton.click();
|
|
|
|
// Wait for dialog and confirm
|
|
const dialog = page.locator(SELECTORS.confirmDialog);
|
|
await expect(dialog).toBeVisible();
|
|
|
|
const confirmButton = dialog.locator(SELECTORS.confirmRestoreButton);
|
|
await confirmButton.click();
|
|
|
|
// Wait for error toast
|
|
await waitForToast(page, /error|failed/i, { type: 'error' });
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Edge Cases Tests (2 tests)
|
|
// =========================================================================
|
|
test.describe('Edge Cases', () => {
|
|
test('should disable restore button while restore is in progress', async ({ page, adminUser }) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
const filename = 'backup_2024-01-15_120000.tar.gz';
|
|
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ status: 200, json: mockBackups });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.route(`**/api/v1/backups/${filename}/restore`, async (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
// Delay response to observe loading state
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: { message: 'Restore completed successfully' },
|
|
});
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.goto('/tasks/backups');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Click restore on first backup
|
|
const restoreButton = page.locator(SELECTORS.restoreBtn).first();
|
|
await restoreButton.click();
|
|
|
|
// Wait for dialog
|
|
const dialog = page.locator(SELECTORS.confirmDialog);
|
|
await expect(dialog).toBeVisible();
|
|
|
|
// Click confirm restore button
|
|
const confirmButton = dialog.locator(SELECTORS.confirmRestoreButton);
|
|
await confirmButton.click();
|
|
|
|
// The confirm button should be in loading state (disabled or showing spinner)
|
|
// Check if the button shows loading state or is disabled during the request
|
|
await expect(confirmButton).toBeDisabled({ timeout: 500 }).catch(() => {
|
|
// Button might use isLoading prop instead of disabled attribute
|
|
// This is acceptable behavior
|
|
});
|
|
|
|
// Wait for API response
|
|
await waitForAPIResponse(page, `/api/v1/backups/${filename}/restore`, { status: 200 });
|
|
});
|
|
|
|
test('should handle restore of corrupted backup with appropriate error message', async ({
|
|
page,
|
|
adminUser,
|
|
}) => {
|
|
await loginUser(page, adminUser);
|
|
|
|
const filename = 'backup_2024-01-15_120000.tar.gz';
|
|
|
|
await page.route('**/api/v1/backups', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ status: 200, json: mockBackups });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.route(`**/api/v1/backups/${filename}/restore`, async (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
await route.fulfill({
|
|
status: 422,
|
|
json: { error: 'Backup file is corrupted or invalid' },
|
|
});
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.goto('/tasks/backups');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Click restore on first backup
|
|
const restoreButton = page.locator(SELECTORS.restoreBtn).first();
|
|
await restoreButton.click();
|
|
|
|
// Wait for dialog and confirm
|
|
const dialog = page.locator(SELECTORS.confirmDialog);
|
|
await expect(dialog).toBeVisible();
|
|
|
|
const confirmButton = dialog.locator(SELECTORS.confirmRestoreButton);
|
|
await confirmButton.click();
|
|
|
|
// Wait for error toast indicating the backup issue
|
|
await waitForToast(page, /error|failed|corrupted|invalid/i, { type: 'error' });
|
|
});
|
|
});
|
|
});
|