216 lines
12 KiB
TypeScript
216 lines
12 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
type Policy = {
|
|
smtp_credentials?: { inherit?: boolean | null };
|
|
imap_credentials?: { inherit?: boolean | null };
|
|
allow_lower_level_limits?: Record<string, boolean>;
|
|
[key: string]: unknown;
|
|
};
|
|
|
|
async function installPolicy(page: Page, options: { parent?: Policy | null; local?: Policy; failFirst?: boolean; failReadOnce?: boolean; holdSave?: Promise<void> } = {}) {
|
|
let current = options.local ?? {};
|
|
const parent = options.parent === null ? null : { smtp_credentials: { inherit: false }, imap_credentials: { inherit: true }, ...options.parent };
|
|
const writes: Policy[] = [];
|
|
const unexpected: string[] = [];
|
|
let reads = 0;
|
|
function response() {
|
|
const effective: Policy = { allow_user_profiles: true, allow_group_profiles: true, allow_campaign_profiles: true, ...parent, ...current };
|
|
for (const protocol of ["smtp", "imap"] as const) {
|
|
const key = `${protocol}_credentials` as const;
|
|
const locked = parent?.allow_lower_level_limits?.[`${key}.inherit`] === false;
|
|
effective[key] = { inherit: (!locked ? current[key]?.inherit : null) ?? parent?.[key]?.inherit ?? true };
|
|
}
|
|
return { policy: current, parent_policy: parent, effective_policy: effective, effective_policy_sources: [
|
|
{ scope_type: "system", label: "System", path: "system", applied_fields: [], policy: parent ?? {} },
|
|
{ scope_type: "tenant", label: "Tenant", path: "tenant", applied_fields: [], policy: current }
|
|
] };
|
|
}
|
|
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
|
|
const request = route.request();
|
|
if (new URL(request.url()).pathname.startsWith("/api/v1/mail/policies/")) {
|
|
if (request.method() === "GET") {
|
|
reads += 1;
|
|
if (options.failReadOnce && reads === 1) {
|
|
await route.fulfill({ status: 503, json: { detail: "Synthetic policy load failed" } }); return;
|
|
}
|
|
await route.fulfill({ json: response() }); return;
|
|
}
|
|
if (request.method() === "PUT") {
|
|
const submitted = request.postDataJSON().policy as Policy;
|
|
writes.push(submitted);
|
|
if (options.holdSave) await options.holdSave;
|
|
if (options.failFirst && writes.length === 1) {
|
|
await route.fulfill({ status: 503, json: { detail: "Synthetic policy write failed" } }); return;
|
|
}
|
|
current = submitted;
|
|
await route.fulfill({ json: response() }); return;
|
|
}
|
|
}
|
|
unexpected.push(`${request.method()} ${new URL(request.url()).pathname}`);
|
|
await route.fulfill({ status: 403, json: { detail: "Unexpected fixture request" } });
|
|
});
|
|
return { writes, unexpected, get reads() { return reads; } };
|
|
}
|
|
|
|
function credentialRows(page: Page) {
|
|
const section = page.getByTestId("mail-credential-policy");
|
|
return {
|
|
section,
|
|
smtp: section.locator(".policy-row").filter({ has: page.locator('select[aria-label="SMTP credential selection"]') }),
|
|
imap: section.locator(".policy-row").filter({ has: page.locator('select[aria-label="IMAP credential selection"]') })
|
|
};
|
|
}
|
|
|
|
test("Mail credential policy exposes inherited/effective choices and persists stable override keys", async ({ page }) => {
|
|
page.on("pageerror", (error) => { throw error; });
|
|
const fixture = await installPolicy(page);
|
|
await page.goto("/?mail-credential-policy&language=en");
|
|
const { smtp, imap } = credentialRows(page);
|
|
await expect(smtp.locator("select")).toHaveValue("inherit");
|
|
await expect(smtp.locator(".policy-effective-value")).toContainText("Require explicit Mail credential");
|
|
// An inherited false does not itself lock the child: only the separate
|
|
// lower-level limit may prevent choosing an inherited default credential.
|
|
await smtp.locator("select").selectOption("profile");
|
|
await imap.locator("select").selectOption("explicit");
|
|
await smtp.getByRole("checkbox", { name: "Allow override" }).focus();
|
|
await page.keyboard.press("Space");
|
|
const wildcard = page.locator(".mail-policy-pattern-row").filter({ hasText: "SMTP hostnames" });
|
|
await wildcard.getByRole("checkbox", { name: "Whitelist", exact: true }).focus();
|
|
await page.keyboard.press("Space");
|
|
const campaignRow = page.locator(".mail-policy-row").filter({ hasText: "Campaign-scoped profiles" });
|
|
await campaignRow.getByRole("checkbox", { name: "Allow override" }).focus();
|
|
await page.keyboard.press("Space");
|
|
await page.getByRole("button", { name: "Save policy", exact: true }).click();
|
|
await expect.poll(() => fixture.writes.length).toBe(1);
|
|
expect(fixture.writes[0].smtp_credentials).toEqual({ inherit: true });
|
|
expect(fixture.writes[0].imap_credentials).toEqual({ inherit: false });
|
|
expect(fixture.writes[0].allow_lower_level_limits).toMatchObject({
|
|
"smtp_credentials.inherit": false, "whitelist.smtp_hosts": false, "allow_campaign_profiles": false
|
|
});
|
|
expect(Object.keys(fixture.writes[0].allow_lower_level_limits ?? {}).some((key) => key.startsWith("i18n:"))).toBe(false);
|
|
await expect(page.getByRole("button", { name: "Save policy", exact: true })).toBeDisabled();
|
|
expect(fixture.unexpected).toEqual([]);
|
|
});
|
|
|
|
test("ancestor credential locks cannot be bypassed or re-enabled and stale local overrides are cleared on save", async ({ page }) => {
|
|
const fixture = await installPolicy(page, {
|
|
parent: { allow_lower_level_limits: { "smtp_credentials.inherit": false } },
|
|
local: { smtp_credentials: { inherit: true }, allow_lower_level_limits: { "smtp_credentials.inherit": true } }
|
|
});
|
|
await page.goto("/?mail-credential-policy&language=en");
|
|
const { smtp, imap, section } = credentialRows(page);
|
|
await expect(smtp.locator("select")).toHaveValue("inherit");
|
|
await expect(smtp.locator("select")).toBeDisabled();
|
|
await expect(smtp.getByRole("checkbox", { name: "Allow override" })).toBeDisabled();
|
|
await expect(smtp.getByRole("checkbox", { name: "Allow override" })).not.toBeChecked();
|
|
await expect(section).toContainText("An ancestor has locked credential selection.");
|
|
await imap.locator("select").selectOption("explicit");
|
|
await page.getByRole("button", { name: "Save policy", exact: true }).click();
|
|
await expect.poll(() => fixture.writes.length).toBe(1);
|
|
expect(fixture.writes[0].smtp_credentials).toEqual({ inherit: null });
|
|
expect(fixture.writes[0].allow_lower_level_limits?.["smtp_credentials.inherit"]).toBeUndefined();
|
|
expect(fixture.unexpected).toEqual([]);
|
|
});
|
|
|
|
test("system defaults are concrete and campaign policy has no lower-level override controls", async ({ page }) => {
|
|
const fixture = await installPolicy(page, { parent: null });
|
|
await page.goto("/?mail-credential-policy&scope=system&language=en");
|
|
let { smtp, imap } = credentialRows(page);
|
|
await expect(smtp.locator("select")).toHaveValue("profile");
|
|
await expect(smtp.locator('option[value="inherit"]')).toHaveCount(0);
|
|
await smtp.locator("select").selectOption("explicit");
|
|
await page.getByRole("button", { name: "Save policy", exact: true }).click();
|
|
await expect.poll(() => fixture.writes.length).toBe(1);
|
|
expect(fixture.writes[0].smtp_credentials).toEqual({ inherit: false });
|
|
expect(fixture.writes[0].imap_credentials).toEqual({ inherit: true });
|
|
expect(fixture.writes[0].allow_lower_level_limits).toHaveProperty("allow_campaign_profiles", true);
|
|
await page.goto("/?mail-credential-policy&scope=campaign&language=en");
|
|
({ smtp, imap } = credentialRows(page));
|
|
await expect(smtp.getByRole("checkbox")).toHaveCount(0);
|
|
await expect(imap.getByRole("checkbox")).toHaveCount(0);
|
|
await smtp.locator("select").selectOption("profile");
|
|
await page.getByRole("button", { name: "Save policy", exact: true }).click();
|
|
await expect.poll(() => fixture.writes.length).toBe(2);
|
|
expect(fixture.writes[1].allow_lower_level_limits).toEqual({});
|
|
expect(fixture.unexpected).toEqual([]);
|
|
});
|
|
|
|
test("read-only and workflow-locked policy scopes cannot change credential selection", async ({ page }) => {
|
|
const fixture = await installPolicy(page);
|
|
for (const blocker of ["read-only", "locked"]) {
|
|
await page.goto(`/?mail-credential-policy&language=en&${blocker}`);
|
|
const { smtp, imap } = credentialRows(page);
|
|
await expect(smtp.locator("select")).toBeDisabled();
|
|
await expect(imap.locator("select")).toBeDisabled();
|
|
await expect(smtp.getByRole("checkbox", { name: "Allow override" })).toBeDisabled();
|
|
await expect(page.getByRole("button", { name: "Save policy", exact: true })).toBeDisabled();
|
|
}
|
|
expect(fixture.writes).toEqual([]);
|
|
expect(fixture.unexpected).toEqual([]);
|
|
});
|
|
|
|
test("successful policy saves remain successful when a dependent refresh fails", async ({ page }) => {
|
|
const fixture = await installPolicy(page);
|
|
await page.goto("/?mail-credential-policy&language=en&refresh-failure");
|
|
await credentialRows(page).smtp.locator("select").selectOption("profile");
|
|
await page.getByRole("button", { name: "Save policy", exact: true }).click();
|
|
await expect(page.getByRole("alert")).toContainText("Mail policy was saved, but refreshing dependent data failed");
|
|
await expect(page.locator(".alert.danger")).toHaveCount(0);
|
|
await expect(page.locator(".alert.success")).toContainText("Mail profile policy saved");
|
|
await expect(page.getByRole("button", { name: "Save policy", exact: true })).toBeDisabled();
|
|
expect(fixture.writes).toHaveLength(1);
|
|
await page.getByRole("button", { name: "Reload", exact: true }).click();
|
|
await expect.poll(() => fixture.reads).toBeGreaterThan(1);
|
|
await expect(credentialRows(page).smtp.locator("select")).toHaveValue("profile");
|
|
expect(fixture.writes).toHaveLength(1);
|
|
expect(fixture.unexpected).toEqual([]);
|
|
});
|
|
|
|
test("failed policy writes retain the draft and require an explicit retry", async ({ page }) => {
|
|
let finish!: () => void;
|
|
const fixture = await installPolicy(page, { failFirst: true, holdSave: new Promise<void>((resolve) => { finish = resolve; }) });
|
|
await page.goto("/?mail-credential-policy&language=en");
|
|
const { smtp } = credentialRows(page);
|
|
await smtp.locator("select").selectOption("profile");
|
|
await page.getByRole("button", { name: "Save policy", exact: true }).click();
|
|
await expect.poll(() => fixture.writes.length).toBe(1);
|
|
await expect(smtp.locator("select")).toBeDisabled();
|
|
finish();
|
|
await expect(page.locator(".alert.danger")).toContainText("Synthetic policy write failed");
|
|
await expect(smtp.locator("select")).toHaveValue("profile");
|
|
await expect(page.getByRole("button", { name: "Save policy", exact: true })).toBeEnabled();
|
|
expect(fixture.writes).toHaveLength(1);
|
|
await page.getByRole("button", { name: "Save policy", exact: true }).click();
|
|
await expect(page.getByRole("button", { name: "Save policy", exact: true })).toBeDisabled();
|
|
expect(fixture.writes).toHaveLength(2);
|
|
expect(fixture.writes[1]).toEqual(fixture.writes[0]);
|
|
expect(fixture.unexpected).toEqual([]);
|
|
});
|
|
|
|
test("failed policy loads cannot enable editing an unknown ancestor policy", async ({ page }) => {
|
|
const fixture = await installPolicy(page, { failReadOnce: true });
|
|
await page.goto("/?mail-credential-policy&language=en");
|
|
await expect(page.locator(".alert.danger")).toContainText("Synthetic policy load failed");
|
|
await expect(credentialRows(page).smtp.locator("select")).toBeDisabled();
|
|
await expect(page.getByRole("button", { name: "Save policy", exact: true })).toBeDisabled();
|
|
await page.getByRole("button", { name: "Reload", exact: true }).click();
|
|
await expect(credentialRows(page).smtp.locator("select")).toBeEnabled();
|
|
expect(fixture.writes).toEqual([]);
|
|
expect(fixture.unexpected).toEqual([]);
|
|
});
|
|
|
|
test("credential selection choices and explanations are available in German", async ({ page }) => {
|
|
const fixture = await installPolicy(page);
|
|
await page.goto("/?mail-credential-policy&language=de");
|
|
const section = page.getByTestId("mail-credential-policy");
|
|
await expect(section.getByRole("heading", { name: "Auswahl der Zugangsdaten", exact: true })).toBeVisible();
|
|
const smtp = section.getByRole("combobox", { name: "SMTP-Zugangsdaten auswählen", exact: true });
|
|
await expect(smtp).toBeEnabled();
|
|
await expect(smtp.locator('option[value="inherit"]')).toHaveText("Richtlinie vom übergeordneten Bereich erben");
|
|
await expect(smtp.locator('option[value="profile"]')).toHaveText("Standard-Zugangsdaten des Profils zulassen");
|
|
await expect(smtp.locator('option[value="explicit"]')).toHaveText("Ausdrückliche Mail-Zugangsdaten verlangen");
|
|
await expect(section).toContainText("Beide Optionen belassen Geheimnisse in Mail");
|
|
expect(fixture.writes).toEqual([]);
|
|
expect(fixture.unexpected).toEqual([]);
|
|
});
|