e2e test setup
This commit is contained in:
@@ -55,3 +55,55 @@ jobs:
|
|||||||
cd frontend
|
cd frontend
|
||||||
npx svelte-kit sync
|
npx svelte-kit sync
|
||||||
npm run check
|
npm run check
|
||||||
|
|
||||||
|
e2e-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [backend-tests, frontend-check]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
version: "latest"
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Install Frontend Dependencies & Playwright
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Start Backend Test Server
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
uv sync --dev
|
||||||
|
DATABASE_URL="sqlite:///test.db" TAPEHOARD_TEST_MODE="true" uv run alembic upgrade head
|
||||||
|
DATABASE_URL="sqlite:///test.db" TAPEHOARD_TEST_MODE="true" uv run uvicorn app.main:app --host 0.0.0.0 --port 8001 &
|
||||||
|
sleep 5 # Wait for server to start
|
||||||
|
|
||||||
|
- name: Run Playwright Tests
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
npx playwright test
|
||||||
|
|
||||||
|
- name: Upload Playwright Report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: frontend/playwright-report/
|
||||||
|
retention-days: 14
|
||||||
|
|||||||
+20
@@ -61,3 +61,23 @@ yarn-error.log*
|
|||||||
/staging/
|
/staging/
|
||||||
/source_data/
|
/source_data/
|
||||||
/config/
|
/config/
|
||||||
|
|
||||||
|
# Testing & Coverage
|
||||||
|
.coverage
|
||||||
|
coverage.xml
|
||||||
|
|
||||||
|
# OpenAPI client errors
|
||||||
|
openapi-ts-error-*.log
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
playwright/.cache/
|
||||||
|
|
||||||
|
# SQLite WAL and SHM
|
||||||
|
*-shm
|
||||||
|
*-wal
|
||||||
|
file:memdb*
|
||||||
|
|
||||||
|
# Docker DB
|
||||||
|
docker/db/
|
||||||
|
|||||||
@@ -61,9 +61,15 @@ def list_storage_providers():
|
|||||||
from app.providers.cloud import CloudStorageProvider
|
from app.providers.cloud import CloudStorageProvider
|
||||||
from app.providers.hdd import OfflineHDDProvider
|
from app.providers.hdd import OfflineHDDProvider
|
||||||
from app.providers.tape import LTOProvider
|
from app.providers.tape import LTOProvider
|
||||||
|
import os
|
||||||
|
|
||||||
providers = [LTOProvider, OfflineHDDProvider, CloudStorageProvider]
|
providers = [LTOProvider, OfflineHDDProvider, CloudStorageProvider]
|
||||||
|
|
||||||
|
if os.environ.get("TAPEHOARD_TEST_MODE") == "true":
|
||||||
|
from app.providers.mock import MockLTOProvider
|
||||||
|
|
||||||
|
providers.append(MockLTOProvider)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
StorageProviderSchema(
|
StorageProviderSchema(
|
||||||
provider_id=p.provider_id,
|
provider_id=p.provider_id,
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from typing import Any, BinaryIO, Dict, Optional
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from .base import AbstractStorageProvider
|
||||||
|
|
||||||
|
|
||||||
|
class MockLTOProvider(AbstractStorageProvider):
|
||||||
|
provider_id = "mock_lto"
|
||||||
|
name = "Mock LTO Tape (Test)"
|
||||||
|
description = "A simulated tape drive for end-to-end testing."
|
||||||
|
capabilities = {
|
||||||
|
"supports_random_access": False,
|
||||||
|
"is_offline_capable": True,
|
||||||
|
"supports_hardware_encryption": False,
|
||||||
|
}
|
||||||
|
config_schema = {
|
||||||
|
"device_path": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Mock Directory Path",
|
||||||
|
"description": "Path to a directory representing the tape drive (e.g., /tmp/mock_lto)",
|
||||||
|
"required": True,
|
||||||
|
"default": "/tmp/mock_lto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any]):
|
||||||
|
self.device_path = config.get("device_path", "/tmp/mock_lto")
|
||||||
|
os.makedirs(self.device_path, exist_ok=True)
|
||||||
|
# We store metadata in a .mam file inside the mock directory
|
||||||
|
self.mam_path = os.path.join(self.device_path, ".mam")
|
||||||
|
|
||||||
|
def get_name(self) -> str:
|
||||||
|
return "Mock LTO Drive"
|
||||||
|
|
||||||
|
def check_online(self, force: bool = False) -> bool:
|
||||||
|
# For testing, we consider it online if the directory exists
|
||||||
|
return os.path.exists(self.device_path)
|
||||||
|
|
||||||
|
def check_existing_data(self) -> bool:
|
||||||
|
# Check if there are archive files in the directory
|
||||||
|
if not self.check_online():
|
||||||
|
return False
|
||||||
|
for f in os.listdir(self.device_path):
|
||||||
|
if f.endswith(".tar"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def identify_media(self) -> Optional[str]:
|
||||||
|
if not os.path.exists(self.mam_path):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(self.mam_path, "r") as f:
|
||||||
|
return f.read().strip()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def initialize_media(self, media_id: str) -> bool:
|
||||||
|
# Clear out the directory
|
||||||
|
for item in os.listdir(self.device_path):
|
||||||
|
item_path = os.path.join(self.device_path, item)
|
||||||
|
if os.path.isfile(item_path):
|
||||||
|
os.remove(item_path)
|
||||||
|
elif os.path.isdir(item_path):
|
||||||
|
shutil.rmtree(item_path)
|
||||||
|
|
||||||
|
# Write the new media_id
|
||||||
|
with open(self.mam_path, "w") as f:
|
||||||
|
f.write(media_id)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def prepare_for_write(self, media_id: str) -> bool:
|
||||||
|
return self.identify_media() == media_id
|
||||||
|
|
||||||
|
def write_archive(self, media_id: str, stream: BinaryIO) -> str:
|
||||||
|
if not self.prepare_for_write(media_id):
|
||||||
|
raise Exception("Media mismatch")
|
||||||
|
|
||||||
|
# Determine the next file number
|
||||||
|
file_num = 0
|
||||||
|
while os.path.exists(os.path.join(self.device_path, f"archive_{file_num}.tar")):
|
||||||
|
file_num += 1
|
||||||
|
|
||||||
|
file_path = os.path.join(self.device_path, f"archive_{file_num}.tar")
|
||||||
|
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
shutil.copyfileobj(stream, f)
|
||||||
|
|
||||||
|
return str(file_num)
|
||||||
|
|
||||||
|
def get_utilization(self) -> Optional[float]:
|
||||||
|
return 0.1 # Mock utilization
|
||||||
|
|
||||||
|
def finalize_media(self, media_id: str):
|
||||||
|
logger.info(f"Mock media {media_id} finalized")
|
||||||
|
|
||||||
|
def read_archive(self, media_id: str, location_id: str) -> BinaryIO:
|
||||||
|
if self.identify_media() != media_id:
|
||||||
|
raise Exception("Media mismatch")
|
||||||
|
|
||||||
|
file_path = os.path.join(self.device_path, f"archive_{location_id}.tar")
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise Exception(f"Archive {location_id} not found on tape")
|
||||||
|
|
||||||
|
return open(file_path, "rb")
|
||||||
|
|
||||||
|
def get_live_info(self, force: bool = False) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"online": self.check_online(force),
|
||||||
|
"mam": {"barcode": self.identify_media()},
|
||||||
|
"drive": {"vendor": "MOCK", "product": "VIRTUAL LTO"},
|
||||||
|
}
|
||||||
@@ -88,6 +88,7 @@ class ArchiverService:
|
|||||||
|
|
||||||
def _get_storage_provider(self, media_record: models.StorageMedia):
|
def _get_storage_provider(self, media_record: models.StorageMedia):
|
||||||
"""Initializes the appropriate hardware provider based on media type."""
|
"""Initializes the appropriate hardware provider based on media type."""
|
||||||
|
import os
|
||||||
|
|
||||||
provider_map = {
|
provider_map = {
|
||||||
LTOProvider.provider_id: LTOProvider,
|
LTOProvider.provider_id: LTOProvider,
|
||||||
@@ -100,6 +101,11 @@ class ArchiverService:
|
|||||||
"s3": CloudStorageProvider,
|
"s3": CloudStorageProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.environ.get("TAPEHOARD_TEST_MODE") == "true":
|
||||||
|
from app.providers.mock import MockLTOProvider
|
||||||
|
|
||||||
|
provider_map[MockLTOProvider.provider_id] = MockLTOProvider
|
||||||
|
|
||||||
provider_cls = provider_map.get(media_record.media_type)
|
provider_cls = provider_map.get(media_record.media_type)
|
||||||
if not provider_cls:
|
if not provider_cls:
|
||||||
return None
|
return None
|
||||||
|
|||||||
Generated
+64
@@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "^0.96.1",
|
"@hey-api/openapi-ts": "^0.96.1",
|
||||||
|
"@playwright/test": "^1.59.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.57.1",
|
"@sveltejs/kit": "^2.57.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||||
@@ -309,6 +310,22 @@
|
|||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.59.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@polka/url": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.29",
|
"version": "1.0.0-next.29",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
@@ -2150,6 +2167,53 @@
|
|||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.59.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.10",
|
"version": "8.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "^0.96.1",
|
"@hey-api/openapi-ts": "^0.96.1",
|
||||||
|
"@playwright/test": "^1.59.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.57.1",
|
"@sveltejs/kit": "^2.57.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: false,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: [['html', { open: 'never' }]],
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: 'http://localhost:5173',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: [
|
||||||
|
{
|
||||||
|
command: 'VITE_API_URL=http://localhost:8001 npm run dev',
|
||||||
|
url: 'http://localhost:5173',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 120 * 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('TapeHoard Golden Path', () => {
|
||||||
|
test('homepage loads and shows basic navigation', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Validate the page title or basic UI elements exist
|
||||||
|
// This assumes there's some header or title indicating TapeHoard
|
||||||
|
await expect(page).toHaveTitle(/TapeHoard|Svelte/i);
|
||||||
|
|
||||||
|
// Check if navigation links are visible
|
||||||
|
// We expect links to Backup Manager, Media Inventory, Archive Index etc based on E2E.md
|
||||||
|
const nav = page.locator('nav');
|
||||||
|
if (await nav.count() > 0) {
|
||||||
|
await expect(page.locator('text=Inventory').first()).toBeVisible();
|
||||||
|
await expect(page.locator('text=Archive').first()).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('media inventory shows mock provider when in test mode', async ({ page }) => {
|
||||||
|
await page.goto('/inventory');
|
||||||
|
|
||||||
|
// Wait for the page to be fully loaded and hydrated
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Click the Register media button to open the dialog
|
||||||
|
await page.getByRole('button', { name: /Register media/i }).click();
|
||||||
|
|
||||||
|
// Check for the Mock provider text inside the dialog
|
||||||
|
await expect(page.getByText('Mock LTO Tape (Test)')).toBeVisible({ timeout: 10000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -89,3 +89,16 @@ docker-up:
|
|||||||
docker-down:
|
docker-down:
|
||||||
@echo "Stopping TapeHoard stack..."
|
@echo "Stopping TapeHoard stack..."
|
||||||
cd docker && docker-compose down
|
cd docker && docker-compose down
|
||||||
|
|
||||||
|
# --- End-to-End Testing ---
|
||||||
|
|
||||||
|
# Run the backend in test mode
|
||||||
|
e2e-server:
|
||||||
|
@echo "Starting Backend in Test Mode..."
|
||||||
|
cd backend && DATABASE_URL="sqlite:///test.db" TAPEHOARD_TEST_MODE="true" uv run alembic upgrade head
|
||||||
|
cd backend && DATABASE_URL="sqlite:///test.db" TAPEHOARD_TEST_MODE="true" uv run uvicorn app.main:app --host 0.0.0.0 --port 8001
|
||||||
|
|
||||||
|
# Run playwright tests
|
||||||
|
e2e:
|
||||||
|
@echo "Running Playwright E2E Tests..."
|
||||||
|
cd frontend && npx playwright test
|
||||||
|
|||||||
Reference in New Issue
Block a user