test(e2e): fix waiting on transitive state
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
This commit is contained in:
parent
f83b765d04
commit
7be7c9edae
5 changed files with 88 additions and 18 deletions
|
|
@ -6,7 +6,7 @@ import {
|
|||
backupScheduleNotificationsTable,
|
||||
type NotificationDestination,
|
||||
} from "../../db/schema";
|
||||
import { logger } from "@zerobyte/core/node";
|
||||
import { logger, sanitizeSensitiveData } from "@zerobyte/core/node";
|
||||
import { isAllowedWebhookUrl } from "@zerobyte/core/backup-hooks";
|
||||
import { sendNotification } from "../../utils/shoutrrr";
|
||||
import { formatDuration } from "~/utils/utils";
|
||||
|
|
@ -20,6 +20,12 @@ import { formatBytes } from "~/utils/format-bytes";
|
|||
import { decryptNotificationConfig, encryptNotificationConfig } from "./notification-config-secrets";
|
||||
import { serverEvents } from "~/server/core/events";
|
||||
|
||||
const MAX_DELIVERY_ERROR_LENGTH = 2048;
|
||||
|
||||
const formatDeliveryError = (error?: string) => {
|
||||
return sanitizeSensitiveData(error ?? "Unknown error").slice(0, MAX_DELIVERY_ERROR_LENGTH);
|
||||
};
|
||||
|
||||
const getCustomShoutrrrWebhookUrl = (shoutrrrUrl: string) => {
|
||||
if (!URL.canParse(shoutrrrUrl)) {
|
||||
return null;
|
||||
|
|
@ -195,7 +201,7 @@ const updateDeliveryStatus = async (destinationId: number, result: { success: bo
|
|||
.set({
|
||||
status: result.success ? "healthy" : "error",
|
||||
lastChecked: Date.now(),
|
||||
lastError: result.success ? null : (result.error ?? "Unknown error"),
|
||||
lastError: result.success ? null : formatDeliveryError(result.error),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(notificationDestinationsTable.id, destinationId))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { safeExec } from "@zerobyte/core/node";
|
||||
import { logger } from "@zerobyte/core/node";
|
||||
import { logger, safeExec, sanitizeSensitiveData } from "@zerobyte/core/node";
|
||||
import { toMessage } from "./errors";
|
||||
|
||||
interface SendNotificationParams {
|
||||
|
|
@ -23,7 +22,7 @@ export async function sendNotification(params: SendNotificationParams) {
|
|||
return { success: true };
|
||||
}
|
||||
|
||||
const errorMessage = result.stderr || result.stdout || "Unknown error";
|
||||
const errorMessage = sanitizeSensitiveData(result.stderr || result.stdout || "Unknown error");
|
||||
logger.error(`Failed to send notification: ${errorMessage}`);
|
||||
return {
|
||||
success: false,
|
||||
|
|
|
|||
|
|
@ -12,10 +12,27 @@ type RepositorySummary = {
|
|||
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;
|
||||
|
|
@ -82,6 +99,25 @@ async function createBackupJob(page: Page, backupName: string, volumeName: strin
|
|||
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}`;
|
||||
|
|
@ -104,6 +140,7 @@ test("can sync missing snapshots to a mirror repository", async ({ page }, testI
|
|||
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);
|
||||
|
||||
|
|
@ -142,20 +179,17 @@ test("can sync missing snapshots to a mirror repository", async ({ page }, testI
|
|||
const syncButton = syncDialog.getByRole("button", { name: "Sync 1 snapshots" });
|
||||
await expect(syncButton).toBeEnabled();
|
||||
|
||||
// Click sync button and wait for the mirror repository to contain the snapshot.
|
||||
// Click sync and wait on the mirror status that backs this dialog and row.
|
||||
await syncButton.click();
|
||||
await gotoAndWaitForAppReady(page, `/repositories/${mirrorRepoShortId}`);
|
||||
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 waitForMirrorSyncComplete(page, backupShortId, mirrorRepoShortId);
|
||||
await gotoAndWaitForAppReady(page, backupPageUrl);
|
||||
|
||||
// Open sync dialog again and verify all snapshots are synced
|
||||
await mirrorRow.getByRole("button").first().click();
|
||||
await expect(syncDialog.getByRole("heading", { name: "Sync snapshots" })).toBeVisible();
|
||||
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();
|
||||
|
||||
|
|
|
|||
|
|
@ -125,6 +125,33 @@ test("sends warning details to the post-backup webhook for a non-zero completed
|
|||
});
|
||||
});
|
||||
|
||||
test("sends warning details to the post-backup webhook for a zero-exit completed backup with warnings", async () => {
|
||||
let postBody: WebhookBody | undefined;
|
||||
|
||||
server.use(
|
||||
http.post("http://localhost:8080/post", async ({ request }) => {
|
||||
postBody = (await request.json()) as WebhookBody;
|
||||
return new HttpResponse(null, { status: 204 });
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runWithHooks({
|
||||
webhooks: {
|
||||
pre: null,
|
||||
post: { url: "http://localhost:8080/post" },
|
||||
},
|
||||
runBackup: () => completedBackup("snapshot-1", 0, "Backup was stopped by the user"),
|
||||
});
|
||||
|
||||
expect(postBody).toMatchObject({ status: "warning", error: "Backup was stopped by the user" });
|
||||
expect(result).toEqual({
|
||||
status: "completed",
|
||||
exitCode: 0,
|
||||
result: "snapshot-1",
|
||||
warningDetails: "Backup was stopped by the user",
|
||||
});
|
||||
});
|
||||
|
||||
test("sends error details to the post-backup webhook when the backup fails", async () => {
|
||||
let postBody: WebhookBody | undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -96,12 +96,16 @@ const appendDetails = (primary: string | null | undefined, next: string | null |
|
|||
return [primary, next].filter(Boolean).join("\n\n");
|
||||
};
|
||||
|
||||
const getCompletedStatus = (exitCode: number, signal: AbortSignal): BackupWebhookStatus => {
|
||||
const getCompletedStatus = (
|
||||
exitCode: number,
|
||||
warningDetails: string | null,
|
||||
signal: AbortSignal,
|
||||
): BackupWebhookStatus => {
|
||||
if (signal.aborted) {
|
||||
return "cancelled";
|
||||
}
|
||||
|
||||
return exitCode === 0 ? "success" : "warning";
|
||||
return exitCode === 0 && !warningDetails ? "success" : "warning";
|
||||
};
|
||||
|
||||
const createAbortController = (timeoutMs: number, signal?: AbortSignal) => {
|
||||
|
|
@ -284,7 +288,7 @@ export const runBackupLifecycle = <TResult>({
|
|||
Effect.map((result) => ({
|
||||
status: "completed" as const,
|
||||
...result,
|
||||
hookStatus: getCompletedStatus(result.exitCode, signal),
|
||||
hookStatus: getCompletedStatus(result.exitCode, result.warningDetails, signal),
|
||||
hookError: signal.aborted ? formatError(signal.reason) : (result.warningDetails ?? undefined),
|
||||
})),
|
||||
Effect.catchAll((error) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue