mirror of
https://github.com/nicotsx/zerobyte.git
synced 2026-06-03 21:59:36 -04:00
201 lines
8.8 KiB
TypeScript
201 lines
8.8 KiB
TypeScript
import fs from "node:fs";
|
|
import { randomUUID } from "node:crypto";
|
|
import path from "node:path";
|
|
import { type Page, type TestInfo } from "@playwright/test";
|
|
import { expect, test } from "./test";
|
|
import { gotoAndWaitForAppReady } from "./helpers/page";
|
|
|
|
const testDataPath = path.join(process.cwd(), "playwright", "temp");
|
|
|
|
type RepositorySummary = {
|
|
name: string;
|
|
shortId: string;
|
|
};
|
|
|
|
type MirrorAssignmentSummary = {
|
|
repositoryId: string;
|
|
lastCopyStatus: "success" | "error" | "in_progress" | null;
|
|
};
|
|
|
|
type MirrorSyncStatus = {
|
|
sourceCount: number;
|
|
mirrorCount: number;
|
|
missingSnapshots: unknown[];
|
|
};
|
|
|
|
function getRunId(testInfo: TestInfo) {
|
|
return `${testInfo.parallelIndex}-${testInfo.retry}-${randomUUID().slice(0, 8)}`;
|
|
}
|
|
|
|
function getBackupShortId(backupPageUrl: string) {
|
|
const backupShortId = new URL(backupPageUrl).pathname.split("/").filter(Boolean).at(-1);
|
|
expect(backupShortId).toBeTruthy();
|
|
return backupShortId!;
|
|
}
|
|
|
|
function getWorkerTestDataPath() {
|
|
fs.mkdirSync(testDataPath, { recursive: true });
|
|
return testDataPath;
|
|
}
|
|
|
|
async function listRepositories(page: Page) {
|
|
const response = await page.request.get("/api/v1/repositories");
|
|
expect(response.ok()).toBe(true);
|
|
return (await response.json()) as RepositorySummary[];
|
|
}
|
|
|
|
async function getRepositoryShortId(page: Page, name: string) {
|
|
const repositories = await listRepositories(page);
|
|
const repository = repositories.find((entry) => entry.name === name);
|
|
expect(repository).toBeDefined();
|
|
return repository!.shortId;
|
|
}
|
|
|
|
async function createRepository(page: Page, name: string) {
|
|
await gotoAndWaitForAppReady(page, "/repositories/create");
|
|
await expect(page.getByRole("textbox", { name: "Name" })).toBeVisible();
|
|
await page.getByRole("textbox", { name: "Name" }).fill(name);
|
|
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({ timeout: 30000 });
|
|
}
|
|
|
|
async function createVolume(page: Page, name: string) {
|
|
await gotoAndWaitForAppReady(page, "/volumes");
|
|
const volumeNameInput = page.getByRole("textbox", { name: "Name" });
|
|
await expect(async () => {
|
|
await page.getByRole("button", { name: "Create Volume" }).click();
|
|
await expect(volumeNameInput).toBeVisible();
|
|
}).toPass({ timeout: 10000 });
|
|
await volumeNameInput.fill(name);
|
|
await page.getByRole("button", { name: "test-data" }).click();
|
|
await page.getByRole("button", { name: "Create Volume" }).click();
|
|
await expect(page.getByText("Volume created successfully")).toBeVisible();
|
|
}
|
|
|
|
async function createBackupJob(page: Page, backupName: string, volumeName: string, repositoryName: string) {
|
|
await gotoAndWaitForAppReady(page, "/backups");
|
|
const createBackupButton = page.getByRole("button", { name: "Create a backup job" }).first();
|
|
if (await createBackupButton.isVisible()) {
|
|
await createBackupButton.click();
|
|
} else {
|
|
await page.getByRole("link", { name: "Create a backup job" }).first().click();
|
|
}
|
|
const volumeSelect = page.getByRole("combobox").filter({ hasText: "Choose a volume to backup" });
|
|
const volumeOption = page.getByRole("option", { name: volumeName });
|
|
await expect(async () => {
|
|
await volumeSelect.click();
|
|
await expect(volumeOption).toBeVisible();
|
|
}).toPass({ timeout: 10000 });
|
|
await volumeOption.click();
|
|
await page.getByRole("textbox", { name: "Backup name" }).fill(backupName);
|
|
await page.getByRole("combobox").filter({ hasText: "Select a repository" }).click();
|
|
await page.getByRole("option", { name: repositoryName }).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();
|
|
}
|
|
|
|
async function waitForMirrorSyncComplete(page: Page, backupShortId: string, mirrorRepoShortId: string) {
|
|
await expect(async () => {
|
|
const mirrorsResponse = await page.request.get(`/api/v1/backups/${backupShortId}/mirrors`);
|
|
expect(mirrorsResponse.ok()).toBe(true);
|
|
const mirrors = (await mirrorsResponse.json()) as MirrorAssignmentSummary[];
|
|
const mirror = mirrors.find((entry) => entry.repositoryId === mirrorRepoShortId);
|
|
expect(mirror?.lastCopyStatus).toBe("success");
|
|
|
|
const statusResponse = await page.request.get(
|
|
`/api/v1/backups/${backupShortId}/mirrors/${mirrorRepoShortId}/status`,
|
|
);
|
|
expect(statusResponse.ok()).toBe(true);
|
|
const status = (await statusResponse.json()) as MirrorSyncStatus;
|
|
expect(status.sourceCount).toBe(1);
|
|
expect(status.mirrorCount).toBe(1);
|
|
expect(status.missingSnapshots).toHaveLength(0);
|
|
}).toPass({ timeout: 30000 });
|
|
}
|
|
|
|
test("can sync missing snapshots to a mirror repository", async ({ page }, testInfo) => {
|
|
const runId = getRunId(testInfo);
|
|
const volumeName = `Volume-${runId}`;
|
|
const primaryRepoName = `Primary-${runId}`;
|
|
const mirrorRepoName = `Mirror-${runId}`;
|
|
const backupName = `Backup-${runId}`;
|
|
|
|
const workerTestDataPath = getWorkerTestDataPath();
|
|
const runPath = path.join(workerTestDataPath, runId);
|
|
fs.mkdirSync(runPath, { recursive: true });
|
|
fs.writeFileSync(path.join(runPath, "test.json"), JSON.stringify({ data: "mirror sync test" }));
|
|
|
|
await gotoAndWaitForAppReady(page, "/");
|
|
await expect(page).toHaveURL("/volumes");
|
|
|
|
// Create volume, two repositories, and a backup job
|
|
await createVolume(page, volumeName);
|
|
await createRepository(page, primaryRepoName);
|
|
await createRepository(page, mirrorRepoName);
|
|
await createBackupJob(page, backupName, volumeName, primaryRepoName);
|
|
|
|
const backupPageUrl = page.url();
|
|
const backupShortId = getBackupShortId(backupPageUrl);
|
|
const primaryRepoShortId = await getRepositoryShortId(page, primaryRepoName);
|
|
const mirrorRepoShortId = await getRepositoryShortId(page, mirrorRepoName);
|
|
|
|
// Run a backup to create a snapshot and wait for it to persist.
|
|
await page.getByRole("button", { name: "Backup now" }).click();
|
|
await gotoAndWaitForAppReady(page, `/repositories/${primaryRepoShortId}`);
|
|
await page.getByRole("tab", { name: "Snapshots" }).click();
|
|
await expect(page.getByText("Backup snapshots stored in this repository.")).toBeVisible();
|
|
await expect(async () => {
|
|
await page.getByRole("button", { name: "Refresh" }).click();
|
|
await expect(page.getByRole("checkbox", { name: /Select snapshot/ })).toHaveCount(1);
|
|
}).toPass({ timeout: 30000 });
|
|
await gotoAndWaitForAppReady(page, backupPageUrl);
|
|
|
|
// Add mirror repository
|
|
await page.getByRole("button", { name: "Add mirror" }).click();
|
|
const mirrorSelect = page.getByRole("combobox").filter({ hasText: "Select a repository to mirror to..." });
|
|
await mirrorSelect.click();
|
|
await page.getByRole("option", { name: mirrorRepoName }).click();
|
|
await page.getByRole("button", { name: "Save changes" }).click();
|
|
await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0);
|
|
|
|
// Click sync button on the mirror row (first icon button in the actions cell)
|
|
const mirrorRow = page.getByRole("row").filter({ hasText: mirrorRepoName });
|
|
await expect(mirrorRow).toBeVisible();
|
|
await mirrorRow.getByRole("button").first().click();
|
|
|
|
// Verify the sync dialog shows missing snapshots
|
|
const syncDialog = page.getByRole("dialog");
|
|
await expect(syncDialog.getByRole("heading", { name: "Sync snapshots" })).toBeVisible();
|
|
await expect(syncDialog.getByText(/1 of 1 snapshots are missing/)).toBeVisible({ timeout: 15000 });
|
|
|
|
// Select the missing snapshots explicitly before syncing.
|
|
await expect(syncDialog.getByRole("button", { name: "Sync 0 snapshots" })).toBeDisabled();
|
|
await syncDialog.getByRole("checkbox").first().click();
|
|
const syncButton = syncDialog.getByRole("button", { name: "Sync 1 snapshots" });
|
|
await expect(syncButton).toBeEnabled();
|
|
|
|
// Click sync and wait on the mirror status that backs this dialog and row.
|
|
await syncButton.click();
|
|
await waitForMirrorSyncComplete(page, backupShortId, mirrorRepoShortId);
|
|
await gotoAndWaitForAppReady(page, backupPageUrl);
|
|
|
|
// Open sync dialog again and verify all snapshots are synced
|
|
await expect(async () => {
|
|
await expect(mirrorRow).toBeVisible();
|
|
await mirrorRow.getByRole("button").first().click();
|
|
await expect(syncDialog.getByRole("heading", { name: "Sync snapshots" })).toBeVisible();
|
|
}).toPass({ timeout: 15000 });
|
|
await expect(syncDialog.getByText(/All 1 snapshots are already synced/)).toBeVisible({ timeout: 15000 });
|
|
await syncDialog.getByRole("button", { name: "Cancel" }).click();
|
|
|
|
// Verify the synced snapshot remains visible in the mirror repository UI.
|
|
await gotoAndWaitForAppReady(page, `/repositories/${mirrorRepoShortId}`);
|
|
await page.getByRole("tab", { name: "Snapshots" }).click();
|
|
await expect(page.getByRole("checkbox", { name: /Select snapshot/ })).toHaveCount(1);
|
|
});
|