Files
backrest/webui/e2e/specs/error-paths.spec.ts

133 lines
6.1 KiB
TypeScript

import { test, expect } from '../harness/fixtures';
import { seedInstance, seedRepo } from '../harness/seed';
/**
* Error-path coverage for the Add Repo and Add Plan dialogs: a wrong-password
* rejection against a real repo, required-field validation, and the
* paths-required rule for plans. In all three cases the primary assertion is
* persistent state (the dialog stays open, nothing new appears in the
* sidebar); the validation/error text is also asserted per the task brief,
* even though it renders in a toast (see "Toast placement" note below).
*
* Toast placement: all of these errors surface via
* webui/src/components/common/Alerts.tsx (`alerts.error` -> Chakra
* `toaster`), which renders in its own top-level Portal
* (webui/src/components/ui/toaster.tsx), not inside the dialog's DOM subtree.
* So error text is asserted at the page level (`page.getByText(...)`), not
* scoped to `dialog`. This is called out because the task brief allows
* "error/validation text within the dialog" for these tests, but the current
* implementation never renders these particular errors inline in the form —
* they are only ever shown as toasts. Flagged as a gap below.
*/
test.describe('error paths', () => {
test('wrong password against an already-initialized repo is rejected and not added', async ({
page,
backrest,
}) => {
await seedInstance(backrest);
await seedRepo(backrest, 'existing-repo');
await page.goto(backrest.url);
await expect(page.getByTestId('sidebar-item-repo-existing-repo')).toBeVisible();
await page.getByTestId('sidebar-add-repo').click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await dialog.getByTestId('add-repo-name').fill('bad-pw');
await dialog.getByTestId('add-repo-uri').fill(backrest.repoPath('existing-repo'));
// Dismiss the URI autocomplete popover by refocusing the name field. Do
// NOT press Escape: when the popover has no suggestions and never opened,
// Escape closes the whole dialog instead.
await dialog.getByTestId('add-repo-name').click();
await dialog.getByTestId('add-repo-password').fill('totally-wrong-password');
await dialog.getByTestId('add-repo-submit').click();
// internal/api/backresthandler.go AddRepo calls RepoOrchestrator.Init
// synchronously, before the repo is appended to config. Against an
// already-initialized repo dir with the wrong password, restic's
// decrypt/unlock fails, `restic init` then also fails because a config
// already exists on disk, and the handler returns
// fmt.Errorf("failed to init repo: %w", err) (pkg/restic/restic.go's
// errAlreadyInitialized = "repo already initialized"). This rejects the
// AddRepo RPC before any config mutation, so the repo is never persisted.
await expect(page.getByText(/failed to init repo/i)).toBeVisible({
timeout: 15_000,
});
await expect(dialog).toBeVisible();
await expect(page.getByTestId('sidebar-item-repo-bad-pw')).toHaveCount(0);
});
test('submitting Add Repo with everything empty shows required-field validation and adds nothing', async ({
page,
backrest,
}) => {
await seedInstance(backrest);
await page.goto(backrest.url);
await page.getByTestId('sidebar-add-repo').click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
// Everything left blank; submit immediately. AddRepoModal's
// validateLocal() checks fields in order and throws on the first
// failure, so only the repo-name error surfaces from this click.
await dialog.getByTestId('add-repo-submit').click();
// en.json: add_repo_modal_error_repo_name_required = "Please input repo name"
await expect(page.getByText(/Please input repo name/i)).toBeVisible();
await expect(dialog).toBeVisible();
await expect(page.locator('[data-testid^="sidebar-item-repo-"]')).toHaveCount(0);
// Fill only the name so the *next* validation rule (URI) fires,
// demonstrating the second add_repo_modal_error_* key as well.
await dialog.getByTestId('add-repo-name').fill('temp-repo');
await dialog.getByTestId('add-repo-submit').click();
// en.json: add_repo_modal_error_uri_required = "Please input repo URI"
await expect(page.getByText(/Please input repo URI/i)).toBeVisible();
await expect(dialog).toBeVisible();
await expect(page.locator('[data-testid^="sidebar-item-repo-"]')).toHaveCount(0);
});
test('submitting Add Plan with no paths surfaces the paths-required validation and adds nothing', async ({
page,
backrest,
}) => {
await seedInstance(backrest);
await seedRepo(backrest, 'existing-repo');
await page.goto(backrest.url);
await page.getByTestId('sidebar-add-plan').click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await dialog.getByTestId('add-plan-name').fill('plan-no-paths');
await dialog.getByTestId('add-plan-repo-select').click();
await page.getByRole('option', { name: 'existing-repo', exact: true }).click();
await expect(dialog.getByTestId('add-plan-repo-select')).toContainText('existing-repo');
// Paths list is left empty; submit.
await dialog.getByTestId('add-plan-submit').click();
// GAP (flagged): en.json's `add_plan_modal_validation_paths_required`
// ("Please enter at least one path to backup") is not referenced anywhere
// in AddPlanModal.tsx's client-side handleOk() validation — it appears to
// be unused/vestigial. The actual paths-required rule lives server-side
// in internal/config/validate.go's validatePlan() ("at least one path is
// required (unless backup_flags supplies paths, e.g. --files-from or
// --stdin-from-command)"), enforced by the SetConfig RPC
// (internal/api/backresthandler.go) and returned as a
// connect.CodeInvalidArgument error, which the modal surfaces via a toast.
await expect(page.getByText(/at least one path is required/i)).toBeVisible({
timeout: 15_000,
});
await expect(dialog).toBeVisible();
await expect(page.getByTestId('sidebar-item-plan-plan-no-paths')).toHaveCount(0);
});
});