Files
tapehoard/frontend/tests/e2e.test.ts
T

167 lines
7.3 KiB
TypeScript

import { test, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
const SOURCE_ROOT = '/tmp/tapehoard_e2e_source';
const MOCK_LTO_PATH = '/tmp/tapehoard_e2e_mock_lto';
const RESTORE_DEST = '/tmp/tapehoard_e2e_restore';
const API_URL = 'http://localhost:8001';
test.describe('TapeHoard Golden Path', () => {
test.beforeAll(async ({ playwright }) => {
// 0. Reset Backend Environment
const requestContext = await playwright.request.newContext();
const resetResponse = await requestContext.post(`${API_URL}/system/test/reset`);
if (!resetResponse.ok()) {
console.error('Failed to reset test environment');
}
// 1. Create source data
if (fs.existsSync(SOURCE_ROOT)) {
fs.rmSync(SOURCE_ROOT, { recursive: true });
}
fs.mkdirSync(SOURCE_ROOT, { recursive: true });
fs.writeFileSync(path.join(SOURCE_ROOT, 'test_file_1.txt'), 'Hello world 1');
fs.mkdirSync(path.join(SOURCE_ROOT, 'subfolder'));
fs.writeFileSync(path.join(SOURCE_ROOT, 'subfolder', 'test_file_2.txt'), 'Hello world 2');
// Create mock LTO dir
if (fs.existsSync(MOCK_LTO_PATH)) {
fs.rmSync(MOCK_LTO_PATH, { recursive: true });
}
fs.mkdirSync(MOCK_LTO_PATH, { recursive: true });
// Ensure restore destination exists
if (fs.existsSync(RESTORE_DEST)) {
fs.rmSync(RESTORE_DEST, { recursive: true });
}
fs.mkdirSync(RESTORE_DEST, { recursive: true });
// Configure backend via API
await requestContext.post(`${API_URL}/system/settings`, {
data: { key: 'source_roots', value: JSON.stringify([SOURCE_ROOT]) }
});
await requestContext.post(`${API_URL}/system/settings`, {
data: { key: 'restore_destinations', value: JSON.stringify([RESTORE_DEST]) }
});
await requestContext.dispose();
});
test('full ingestion, archival, and recovery workflow', async ({ page }) => {
page.on('console', msg => console.log('BROWSER CONSOLE:', msg.text()));
page.on('pageerror', err => console.log('BROWSER ERROR:', err.message));
console.log('Step 1: Discovery & Tracking');
await page.goto('/filesystem');
await page.waitForLoadState('networkidle');
await expect(page.getByText(SOURCE_ROOT).first()).toBeVisible();
await page.getByText(SOURCE_ROOT).first().click();
await page.waitForLoadState('networkidle');
await expect(page.getByText('test_file_1.txt')).toBeVisible();
await expect(page.getByText('subfolder')).toBeVisible();
const fileRow1 = page.locator('div[role="button"]', { hasText: 'test_file_1.txt' });
await fileRow1.locator('div').first().click();
await page.getByRole('button', { name: /Commit rules/i }).click();
await expect(page.getByText(/Changes committed/i)).toBeVisible();
console.log('Step 2: Indexing');
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: /Start scan/i }).click();
await expect(page.getByText(/Scan job initiated/i)).toBeVisible();
await expect(async () => {
await page.getByRole('button', { name: /Refresh/i }).click();
const monitoredCountText = await page.locator('h4').first().textContent();
const monitoredCount = parseInt(monitoredCountText?.replace(/,/g, '') || '0');
console.log(`Current monitored count: ${monitoredCount}`);
expect(monitoredCount).toBeGreaterThan(0);
}).toPass({ timeout: 20000 });
console.log('Step 3: Media Registration');
await page.goto('/inventory');
await page.waitForLoadState('networkidle');
console.log('Clicking Register media button');
await page.getByRole('button', { name: /Register media/i }).click();
console.log('Waiting for Mock LTO Tape text');
await expect(page.getByText('Mock LTO Tape (Test)')).toBeVisible({ timeout: 10000 });
await page.getByText('Mock LTO Tape (Test)').click();
console.log('Filling form');
await page.getByLabel('Identifier (Barcode/SN)').fill('TAPE001');
await page.getByLabel('Capacity (GB)').fill('100');
await page.getByLabel('Mock Directory Path').fill(MOCK_LTO_PATH);
await page.getByRole('button', { name: 'Register media' }).last().click();
await expect(page.getByText(/TAPE001 registered/i)).toBeVisible();
console.log('Step 4: Initialization');
page.on('dialog', dialog => {
console.log('Dialog opened: ', dialog.message());
dialog.accept();
});
await page.getByRole('button', { name: /Initialize/i }).click();
await expect(page.getByText(/initialized successfully/i)).toBeVisible({ timeout: 10000 });
console.log('Step 5: Archival');
await expect(page.getByText('TAPE001', { exact: true })).toBeVisible();
await page.getByRole('button', { name: /Auto archive/i }).click();
await expect(page.getByText(/Archival job initiated/i)).toBeVisible();
console.log('Step 6: Waiting for archival job');
await page.goto('/jobs');
const backupJob = page.locator('div', { hasText: /BACKUP/i }).filter({ hasText: /JOB #/ }).first();
await expect(backupJob.getByText('COMPLETED', { exact: true }).first()).toBeVisible({ timeout: 60000 });
console.log('Step 7: Verify Protection');
await page.goto('/index-browser');
await page.waitForLoadState('networkidle');
await page.getByText(SOURCE_ROOT).first().dblclick();
await page.getByText('subfolder').first().dblclick();
await expect(page.getByText('test_file_2.txt')).toBeVisible();
await expect(page.getByText('TAPE001')).toBeVisible();
console.log('Step 8: Data Recovery');
const fileRow = page.locator('div[role="button"]', { hasText: 'test_file_2.txt' });
await fileRow.locator('button[role="checkbox"]').click();
await expect(page.getByText(/2 items in queue/i)).toBeVisible();
await page.goto('/restores');
await page.waitForLoadState('networkidle');
await page.getByRole("treeitem").getByText('/tmp/tapehoard_e2e_source').click();
await page.waitForLoadState('networkidle');
await page.getByText('subfolder').dblclick();
await expect(page.getByText('test_file_2.txt')).toBeVisible();
await page.locator('select#destination').selectOption(RESTORE_DEST);
await page.getByRole('button', { name: /Initiate recovery/i }).click();
await expect(page.getByText(/Recovery job initiated/i)).toBeVisible();
console.log('Step 9: Waiting for restore job');
await page.goto('/jobs');
const restoreJob = page.locator('div', { hasText: /RESTORE/i }).filter({ hasText: /JOB #/ }).first();
await expect(restoreJob.getByText('COMPLETED', { exact: true }).first()).toBeVisible({ timeout: 60000 });
console.log('Step 10: Verify disk');
const restoredFilePath = path.join(RESTORE_DEST, SOURCE_ROOT, 'subfolder', 'test_file_2.txt');
await page.waitForTimeout(2000);
if (!fs.existsSync(restoredFilePath)) {
const fallbackPath = path.join(RESTORE_DEST, 'test_file_2.txt');
if (fs.existsSync(fallbackPath)) {
expect(fs.readFileSync(fallbackPath, 'utf-8')).toBe('Hello world 2');
} else {
const files = fs.readdirSync(RESTORE_DEST, { recursive: true });
console.log('Restore DEST contents:', files);
throw new Error(`Restored file not found. Present: ${files.join(', ')}`);
}
} else {
expect(fs.readFileSync(restoredFilePath, 'utf-8')).toBe('Hello world 2');
}
});
});