From 1ea2460ea7e8e3dd073d2761e08f7bbdcbdc3be7 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:00:29 +0100 Subject: [PATCH] e2e: test backup & restore functions (#341) --- .gitignore | 3 + app/client/components/restore-form.tsx | 6 +- docker-compose.yml | 3 +- ...itial-setup.spec.ts => 0001-auth.setup.ts} | 16 ++++ e2e/0002-backup-restore.spec.ts | 73 +++++++++++++++++++ e2e/helpers/db.ts | 2 +- playwright.config.ts | 10 ++- 7 files changed, 106 insertions(+), 7 deletions(-) rename e2e/{0001-initial-setup.spec.ts => 0001-auth.setup.ts} (83%) create mode 100644 e2e/0002-backup-restore.spec.ts diff --git a/.gitignore b/.gitignore index 329fb07b..d0a5ab04 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ node_modules/ /blob-report/ /playwright/.cache/ /playwright/.auth/ + +playwright/.auth +playwright/temp diff --git a/app/client/components/restore-form.tsx b/app/client/components/restore-form.tsx index 37d6d55e..e2594232 100644 --- a/app/client/components/restore-form.tsx +++ b/app/client/components/restore-form.tsx @@ -98,10 +98,8 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re const { mutate: restoreSnapshot, isPending: isRestoring } = useMutation({ ...restoreSnapshotMutation(), - onSuccess: (data) => { - toast.success("Restore completed", { - description: `Successfully restored ${data.filesRestored} file(s). ${data.filesSkipped} file(s) skipped.`, - }); + onSuccess: () => { + toast.success("Restore completed"); void navigate(returnPath); }, onError: (error) => { diff --git a/docker-compose.yml b/docker-compose.yml index fa3d629b..424f5d3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,4 +57,5 @@ services: - "4096:4096" volumes: - /etc/localtime:/etc/localtime:ro - - ./data:/var/lib/zerobyte + - ./playwright/data:/var/lib/zerobyte/data + - ./playwright/temp:/test-data diff --git a/e2e/0001-initial-setup.spec.ts b/e2e/0001-auth.setup.ts similarity index 83% rename from e2e/0001-initial-setup.spec.ts rename to e2e/0001-auth.setup.ts index 38251896..72fb6626 100644 --- a/e2e/0001-initial-setup.spec.ts +++ b/e2e/0001-auth.setup.ts @@ -1,6 +1,9 @@ import fs from "fs"; import { test, expect } from "@playwright/test"; import { resetDatabase } from "./helpers/db"; +import path from "node:path"; + +const authFile = path.join(process.cwd(), "./playwright/.auth/user.json"); // TODO: Run these tests with different users once multi-user support is added @@ -80,3 +83,16 @@ test("can't create another admin user after initial setup", async ({ page }) => await expect(page.getByText("Failed to create admin user")).toBeVisible(); }); + +test("can login after initial setup", async ({ page }) => { + await page.goto("/login"); + + await page.getByRole("textbox", { name: "Username" }).fill("test"); + await page.getByRole("textbox", { name: "Password" }).fill("password"); + await page.getByRole("button", { name: "Login" }).click(); + + await expect(page).toHaveURL("/volumes"); + await expect(page.getByRole("heading", { name: "No volume" })).toBeVisible(); + + await page.context().storageState({ path: authFile }); +}); diff --git a/e2e/0002-backup-restore.spec.ts b/e2e/0002-backup-restore.spec.ts new file mode 100644 index 00000000..cedcc7ea --- /dev/null +++ b/e2e/0002-backup-restore.spec.ts @@ -0,0 +1,73 @@ +import { expect, test } from "@playwright/test"; +import path from "node:path"; +import fs from "node:fs"; + +test.beforeAll(() => { + const testDataPath = path.join(process.cwd(), "playwright", "temp"); + if (fs.existsSync(testDataPath)) { + fs.rmSync(testDataPath, { recursive: true, force: true }); + } +}); + +test("can backup & restore a file", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveURL("/volumes"); + + // 0. Create a test file in /test-data + const testDataPath = path.join(process.cwd(), "playwright", "temp"); + if (!fs.existsSync(testDataPath)) { + fs.mkdirSync(testDataPath); + } + const filePath = path.join(testDataPath, "test.json"); + fs.chmodSync(testDataPath, 0o777); + fs.writeFileSync(filePath, JSON.stringify({ data: "test file" })); + + // 1. Create a local volume on /test-data + await page.getByRole("button", { name: "Create Volume" }).click(); + await page.getByRole("textbox", { name: "Name" }).fill("Test Volume"); + await page.getByRole("button", { name: "Change" }).click(); + await page.getByRole("button", { name: "test-data" }).click(); + await page.getByRole("button", { name: "Create Volume" }).click(); + await expect(page.getByText("Volume created successfully")).toBeVisible(); + + // 2. Create a local repository on the default location + await page.getByRole("link", { name: "Repositories" }).click(); + await page.getByRole("button", { name: "Create repository" }).click(); + await page.getByRole("textbox", { name: "Name" }).fill("Test Repo"); + await page.getByRole("combobox", { name: "Backend" }).click(); + await page.getByRole("option", { name: "Local" }).click(); + await page.getByRole("button", { name: "Create repository" }).click(); + await expect(page.getByText("Repository created successfully")).toBeVisible(); + + // 3. Create a backup schedule + await page.getByRole("link", { name: "Backups" }).click(); + await page.getByRole("button", { name: "Create a backup job" }).click(); + await page.getByRole("combobox").filter({ hasText: "Choose a volume to backup" }).click(); + await page.getByRole("option", { name: "test-volume" }).click(); + await page.getByRole("textbox", { name: "Backup name" }).fill("Test Backup"); + await page.getByRole("combobox").filter({ hasText: "Select a repository" }).click(); + await page.getByRole("option", { name: "Test Repo" }).click(); + await page.getByRole("combobox").filter({ hasText: "Select frequency" }).click(); + await page.getByRole("option", { name: "Daily" }).click(); + await page.getByRole("textbox", { name: "Execution time" }).fill("00:00"); + await page.getByRole("button", { name: "Create" }).click(); + await expect(page.getByText("Backup job created successfully")).toBeVisible(); + + // 4. Runs that schedule once + await page.getByRole("button", { name: "Backup now" }).click(); + await expect(page.getByText("Backup started successfully")).toBeVisible(); + await expect(page.getByText("✓ Success")).toBeVisible({ timeout: 30000 }); + + // 5. Modify the json file after the backup + fs.writeFileSync(filePath, JSON.stringify({ data: "modified file" })); + + // 6. Restores the file from backup + await page.getByRole("link", { name: "Restore" }).click(); + await expect(page).toHaveURL(/\/restore/); + await page.getByRole("button", { name: "Restore All" }).click(); + await expect(page.getByText("Restore completed")).toBeVisible({ timeout: 30000 }); + + // 7. Ensures that the file is back to its previous state + const restoredContent = fs.readFileSync(filePath, "utf8"); + expect(JSON.parse(restoredContent)).toEqual({ data: "test file" }); +}); diff --git a/e2e/helpers/db.ts b/e2e/helpers/db.ts index 3c0068ac..bec09ce1 100644 --- a/e2e/helpers/db.ts +++ b/e2e/helpers/db.ts @@ -4,7 +4,7 @@ import path from "node:path"; import { DATABASE_URL } from "~/server/core/constants"; import * as schema from "~/server/db/schema"; -const sqlite = createClient({ url: `file:${path.join(process.cwd(), "data", DATABASE_URL)}` }); +const sqlite = createClient({ url: `file:${path.join(process.cwd(), "playwright", DATABASE_URL)}` }); export const db = drizzle({ client: sqlite, schema: schema }); diff --git a/playwright.config.ts b/playwright.config.ts index 1bb90c36..7b4797d9 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -14,9 +14,17 @@ export default defineConfig({ trace: "on-first-retry", }, projects: [ + { + name: "setup", + testMatch: /.*\.setup\.ts/, + }, { name: "chromium", - use: { ...devices["Desktop Chrome"] }, + use: { + ...devices["Desktop Chrome"], + storageState: "playwright/.auth/user.json", + }, + dependencies: ["setup"], }, // {