167 lines
7.3 KiB
TypeScript
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');
|
|
}
|
|
});
|
|
});
|