197 lines
11 KiB
TypeScript
197 lines
11 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
const zip = { enabled: true, archives: [{
|
|
id: "legacy-zip", name: "Existing recipient archive", method: "zip_standard",
|
|
password_enabled: true, password_delivery_channel: "separate_mail"
|
|
}] };
|
|
const defaultCredential = {
|
|
id: "credential-default", name: "Default account", is_active: true, is_default: true,
|
|
public_data: { username: "sender@example.test" }
|
|
};
|
|
const alternateCredential = {
|
|
id: "credential-alternate", name: "Explicit alternate account", is_active: true, is_default: false,
|
|
public_data: { username: "alternate@example.test" }
|
|
};
|
|
const inactiveCredential = {
|
|
id: "credential-inactive", name: "Inactive account", is_active: false, is_default: false,
|
|
public_data: { username: "inactive@example.test" }
|
|
};
|
|
const profile = {
|
|
id: "profile-1", name: "Campaign delivery", scope_type: "tenant", scope_id: "tenant-1", is_active: true,
|
|
servers: [{
|
|
id: "smtp-1", name: "Active SMTP server", protocol: "smtp", is_active: true, is_default: true,
|
|
config: { host: "smtp.example.test", port: 587, security: "starttls" },
|
|
credentials: [defaultCredential, alternateCredential, inactiveCredential]
|
|
}, {
|
|
id: "smtp-inactive", name: "Inactive SMTP server", protocol: "smtp", is_active: false, is_default: false,
|
|
config: { host: "inactive.example.test", port: 587, security: "starttls" },
|
|
credentials: [defaultCredential]
|
|
}]
|
|
};
|
|
|
|
async function install(page: Page, options: {
|
|
language?: "en" | "de";
|
|
selectedServer?: Record<string, string>;
|
|
excludeProfile?: boolean;
|
|
requireCredential?: boolean;
|
|
} = {}) {
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (error) => { errors.push(error.message); });
|
|
let revision = 1;
|
|
let migrationRequired = true;
|
|
let raw: Record<string, unknown> = {
|
|
version: "1.0", campaign: { id: "campaign-mail", name: "Mail repair fixture", mode: "send" },
|
|
server: options.selectedServer ?? { mail_profile_id: "profile-1", smtp_server_id: "smtp-1" },
|
|
template: { subject: "Fixture subject", text: "Fixture body" },
|
|
recipients: { from: [{ email: "sender@example.test" }] }, entries: { inline: [] },
|
|
attachments: { zip }, delivery: { imap_append_sent: { enabled: false, folder: "auto" } }
|
|
};
|
|
const writes: Record<string, any>[] = [];
|
|
const reads: URL[] = [];
|
|
const unexpectedWrites: string[] = [];
|
|
const version = () => ({
|
|
id: "version-mail", campaign_id: "campaign-mail", version_number: 2,
|
|
schema_version: "1.0", edit_revision: revision, strong_etag: `"version-mail:${revision}"`,
|
|
current_flow: "manual", current_step: "mail-settings", workflow_state: "editing",
|
|
is_complete: false, editor_state: { created_from: "minimal_campaign" },
|
|
user_lock_state: null, locked_at: null, published_at: null,
|
|
created_at: "2026-09-07T10:00:00Z", updated_at: "2026-09-07T10:00:00Z",
|
|
raw_json: raw, mail_profile_migration_required: migrationRequired
|
|
});
|
|
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
|
|
const request = route.request(); const url = new URL(request.url());
|
|
if (request.method() === "GET") {
|
|
reads.push(url);
|
|
if (url.pathname.endsWith("/workspace/delta")) return route.fulfill({ json: {
|
|
campaign: { id: "campaign-mail", name: "Mail repair fixture", current_version_id: "version-mail", status: "draft" },
|
|
versions: [version()], current_version: version(), summary: null, deleted: [],
|
|
watermark: `mail-watermark-${revision}`, has_more: false, full: true
|
|
} });
|
|
if (url.pathname === "/api/v1/mail/profiles") return route.fulfill({ json: {
|
|
profiles: options.excludeProfile ? [] : [profile]
|
|
} });
|
|
if (url.pathname.endsWith("/versions/version-mail")) return route.fulfill({ json: version() });
|
|
return route.fulfill({ json: {} });
|
|
}
|
|
if (request.method() === "POST" && url.pathname.endsWith("/versions/version-mail/autosave")) {
|
|
const body = request.postDataJSON(); writes.push(body);
|
|
if (options.requireCredential && !body.campaign_json.server.smtp_credential_id) {
|
|
return route.fulfill({ status: 422, json: {
|
|
detail: "Campaign delivery cannot use the selected profile because the effective SMTP credential policy requires an explicit credential selection for this campaign."
|
|
} });
|
|
}
|
|
if (body.base_revision !== revision) return route.fulfill({ status: 409, json: { detail: {
|
|
code: "revision_conflict", resource: { type: "campaign_version", id: "version-mail" },
|
|
current_revision: revision, submitted_base_revision: body.base_revision
|
|
} } });
|
|
raw = body.campaign_json;
|
|
revision += 1;
|
|
if (body.migrate_legacy_mail_settings) migrationRequired = false;
|
|
return route.fulfill({ json: version() });
|
|
}
|
|
unexpectedWrites.push(`${request.method()} ${url.pathname}`);
|
|
return route.abort();
|
|
});
|
|
await page.goto(`/?campaign-mail-settings&language=${options.language ?? "en"}`);
|
|
await expect(page.getByRole("combobox", { name: "SMTP credential", exact: true })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: /Reload profiles|Profile neu laden/ })).toBeEnabled();
|
|
return { writes, reads, errors, unexpectedWrites, raw: () => raw, revision: () => revision };
|
|
}
|
|
|
|
for (const language of ["en", "de"] as const) {
|
|
test(`real Mail settings never display inherited defaults as explicit saved credentials in ${language}`, async ({ page }) => {
|
|
const fixture = await install(page, { language, requireCredential: true });
|
|
const credential = page.getByRole("combobox", { name: "SMTP credential", exact: true });
|
|
await expect(credential).toHaveValue("");
|
|
await expect(credential.locator("option:checked")).toContainText(language === "en" ? "Use profile credentials" : "Profil-Zugangsdaten verwenden");
|
|
await expect(credential.locator('option[value="credential-default"]')).toHaveCount(1);
|
|
expect(fixture.writes).toHaveLength(0);
|
|
await credential.selectOption("credential-default");
|
|
await page.getByRole("button", { name: language === "en" ? "Migrate to selected Mail profile" : "Auf ausgewähltes Mail-Profil umstellen", exact: true }).click();
|
|
await expect.poll(() => fixture.revision()).toBe(2);
|
|
await expect(page.getByRole("button", { name: language === "en" ? "Migrate to selected Mail profile" : "Auf ausgewähltes Mail-Profil umstellen", exact: true })).toHaveCount(0);
|
|
expect(fixture.writes).toHaveLength(1);
|
|
expect(fixture.writes[0].migrate_legacy_mail_settings).toBe(true);
|
|
expect(fixture.writes[0].campaign_json.server).toEqual({
|
|
mail_profile_id: "profile-1", smtp_server_id: "smtp-1", smtp_credential_id: "credential-default"
|
|
});
|
|
expect(fixture.writes[0].campaign_json.attachments.zip).toEqual(zip);
|
|
expect(fixture.writes[0].base_revision).toBe(1);
|
|
expect(fixture.unexpectedWrites).toEqual([]);
|
|
expect(fixture.errors).toEqual([]);
|
|
await page.reload();
|
|
await expect(credential).toHaveValue("credential-default");
|
|
expect(fixture.writes).toHaveLength(1);
|
|
});
|
|
}
|
|
|
|
test("explicit-credential 422 preserves the real Mail draft and can be corrected then saved", async ({ page }) => {
|
|
const fixture = await install(page, { requireCredential: true });
|
|
await page.getByRole("button", { name: "Migrate to selected Mail profile", exact: true }).click();
|
|
await expect(page.getByText(/effective SMTP credential policy requires an explicit credential selection/)).toBeVisible();
|
|
expect(fixture.revision()).toBe(1);
|
|
expect(fixture.writes).toHaveLength(1);
|
|
await expect(page.getByRole("combobox", { name: "SMTP server", exact: true })).toHaveValue("smtp-1");
|
|
await expect(page.getByRole("combobox", { name: "SMTP credential", exact: true })).toHaveValue("");
|
|
await expect(page.getByRole("button", { name: "Migrate to selected Mail profile", exact: true })).toBeEnabled();
|
|
await page.getByRole("combobox", { name: "SMTP credential", exact: true }).selectOption("credential-alternate");
|
|
await page.getByRole("button", { name: "Save", exact: true }).click();
|
|
await expect.poll(() => fixture.revision()).toBe(2);
|
|
await expect(page.getByRole("button", { name: "Migrate to selected Mail profile", exact: true })).toHaveCount(0);
|
|
expect(fixture.writes).toHaveLength(2);
|
|
expect(fixture.writes[0].campaign_json.server.smtp_credential_id).toBeUndefined();
|
|
expect(fixture.writes[1].campaign_json.server.smtp_credential_id).toBe("credential-alternate");
|
|
expect(fixture.writes[1].campaign_json.server.smtp_server_id).toBe("smtp-1");
|
|
expect(fixture.writes[1].campaign_json.attachments.zip).toEqual(zip);
|
|
expect(fixture.errors).toEqual([]);
|
|
expect(fixture.unexpectedWrites).toEqual([]);
|
|
});
|
|
|
|
for (const credentialId of ["credential-missing", "credential-inactive"]) {
|
|
test(`a stored ${credentialId} stays visibly unavailable until explicitly replaced`, async ({ page }) => {
|
|
const fixture = await install(page, { selectedServer: {
|
|
mail_profile_id: "profile-1", smtp_server_id: "smtp-1", smtp_credential_id: credentialId
|
|
} });
|
|
const credential = page.getByRole("combobox", { name: "SMTP credential", exact: true });
|
|
await expect(credential).toHaveValue(credentialId);
|
|
await expect(credential.locator("option:checked")).toHaveText("Selected credential is unavailable");
|
|
await expect(credential.locator('option[value="credential-inactive"]')).toHaveCount(credentialId === "credential-inactive" ? 1 : 0);
|
|
expect(fixture.writes).toHaveLength(0);
|
|
await credential.selectOption("credential-default");
|
|
await page.getByRole("button", { name: "Save", exact: true }).click();
|
|
await expect.poll(() => fixture.revision()).toBe(2);
|
|
expect(fixture.writes[0].campaign_json.server.smtp_credential_id).toBe("credential-default");
|
|
expect(fixture.errors).toEqual([]);
|
|
});
|
|
}
|
|
|
|
for (const serverId of ["smtp-missing", "smtp-inactive"]) {
|
|
test(`a stored ${serverId} cannot silently fall back to the default SMTP endpoint`, async ({ page }) => {
|
|
const fixture = await install(page, { selectedServer: {
|
|
mail_profile_id: "profile-1", smtp_server_id: serverId, smtp_credential_id: "credential-default"
|
|
} });
|
|
const server = page.getByRole("combobox", { name: "SMTP server", exact: true });
|
|
await expect(server).toHaveValue(serverId);
|
|
await expect(server.locator("option:checked")).toContainText("unavailable");
|
|
await expect(page.getByRole("combobox", { name: "SMTP credential", exact: true })).toBeDisabled();
|
|
expect(fixture.writes).toHaveLength(0);
|
|
await server.selectOption("smtp-1");
|
|
await page.getByRole("button", { name: "Save", exact: true }).click();
|
|
await expect.poll(() => fixture.revision()).toBe(2);
|
|
expect(fixture.writes[0].campaign_json.server).toEqual({
|
|
mail_profile_id: "profile-1", smtp_server_id: "smtp-1", smtp_credential_id: "credential-default"
|
|
});
|
|
expect(fixture.errors).toEqual([]);
|
|
});
|
|
}
|
|
|
|
test("an unavailable profile remains identified and cannot be silently migrated", async ({ page }) => {
|
|
const fixture = await install(page, { excludeProfile: true });
|
|
await expect(page.getByRole("combobox", { name: "Profile", exact: true })).toHaveValue("profile-1");
|
|
await expect(page.getByRole("button", { name: "Migrate to selected Mail profile", exact: true })).toBeDisabled();
|
|
await expect(page.getByRole("combobox", { name: "SMTP server", exact: true })).toBeDisabled();
|
|
expect(fixture.writes).toHaveLength(0);
|
|
expect(fixture.reads.filter((url) => url.pathname === "/api/v1/mail/profiles").every((url) => url.searchParams.get("campaign_id") === "campaign-mail")).toBe(true);
|
|
expect(fixture.unexpectedWrites).toEqual([]);
|
|
});
|