116 lines
5.8 KiB
TypeScript
116 lines
5.8 KiB
TypeScript
import { campaignJsonForAttachmentPreview } from "../src/features/campaigns/utils/templatePreviewDraft";
|
|
import { checkContentLibraryCompatibility } from "../src/features/campaigns/utils/contentLibraryCompatibility";
|
|
import {
|
|
campaignVersionUpdateForRequest,
|
|
clientCampaignEditorState
|
|
} from "../src/features/campaigns/utils/editorState";
|
|
|
|
function assert(condition: unknown, message = "assertion failed"): void {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
}
|
|
|
|
const draft = {
|
|
recipients: {
|
|
from: { name: "", email: "" },
|
|
to: [{ name: "Bob", email: " bob@example.org " }, { name: "", email: "" }]
|
|
},
|
|
entries: {
|
|
inline: [{
|
|
id: "recipient-1",
|
|
active: true,
|
|
name: "",
|
|
email: "",
|
|
from: [],
|
|
to: [{ name: "", email: "" }, { name: "Alice", email: " alice@example.org " }],
|
|
cc: [],
|
|
bcc: [],
|
|
merge_to: false
|
|
}],
|
|
defaults: {
|
|
name: "",
|
|
email: "",
|
|
to: [{ name: "", email: "" }],
|
|
attachments: []
|
|
}
|
|
}
|
|
};
|
|
|
|
const normalized = campaignJsonForAttachmentPreview(draft);
|
|
const entries = asRecord(normalized.entries);
|
|
const inline = entries.inline as Record<string, unknown>[];
|
|
const entry = inline[0];
|
|
const to = entry.to as Record<string, unknown>[];
|
|
const defaults = asRecord(entries.defaults);
|
|
const recipients = asRecord(normalized.recipients);
|
|
const globalTo = recipients.to as Record<string, unknown>[];
|
|
|
|
assert(recipients.from === undefined, "empty global from object is stripped");
|
|
assert(globalTo.length === 1 && globalTo[0].email === "bob@example.org", "global recipient arrays are sanitized");
|
|
assert(entry.email === undefined, "empty legacy entry email is stripped");
|
|
assert(entry.name === undefined, "empty legacy entry name is stripped");
|
|
assert(Array.isArray(entry.from) && (entry.from as unknown[]).length === 0, "empty address arrays remain arrays");
|
|
assert(to.length === 1 && to[0].email === "alice@example.org", "empty recipient objects are removed and valid addresses are trimmed");
|
|
assert(defaults.email === undefined, "empty defaults email is stripped");
|
|
assert(Array.isArray(defaults.to) && (defaults.to as unknown[]).length === 0, "empty defaults recipients are removed");
|
|
assert((asRecord(draft.entries).inline as Record<string, unknown>[])[0].email === "", "original draft is not mutated");
|
|
|
|
const compatibility = checkContentLibraryCompatibility([
|
|
{ path: "local::display_name", value_type: "string", label: "Display name", required: true },
|
|
{ path: "global.case_reference", value_type: "string", label: "Case reference", required: true },
|
|
{ path: "count", value_type: "number", label: "Count", required: true },
|
|
{ path: "local.to.2.email", value_type: "string", label: "Second recipient", required: true },
|
|
{ path: "optional", value_type: "string", required: false }
|
|
], new Map([
|
|
["local:display_name", "string"],
|
|
["local:to", "string"],
|
|
["count", "boolean"]
|
|
]));
|
|
assert(!compatibility.compatible, "missing or wrong-typed fields are incompatible");
|
|
assert(compatibility.missing.length === 1 && compatibility.missing[0].path === "global:case_reference", "namespaced missing fields are normalized");
|
|
assert(compatibility.incompatible.length === 1 && compatibility.incompatible[0].actual === "boolean", "type mismatches are reported");
|
|
|
|
// Ordinary saves must not echo the server-owned evidence returned by reads.
|
|
// This is the state that previously broke saves when leaving the Template page.
|
|
const loadedEditorState = {
|
|
created_from: "minimal_campaign",
|
|
field_overrides: { department: false },
|
|
opt_ins: { inline_guidance: true },
|
|
review_send: {
|
|
inspection_complete: true,
|
|
reviewed_message_keys: ["gabriele.fode", "fruzsina.molnar-gabor"],
|
|
issue_decisions: [],
|
|
updated_at: "2026-07-15T11:23:31.569257+00:00",
|
|
updated_by_user_id: "064fa105-9e69-4f29-8558-a7665eb219df"
|
|
},
|
|
approval_gate: { request_id: "approval-1", requested_by_user_id: "approver-1" },
|
|
smtp: { password: "legacy-secret" }
|
|
};
|
|
const clientState = clientCampaignEditorState(loadedEditorState);
|
|
assert(JSON.stringify(clientState) === JSON.stringify({
|
|
created_from: "minimal_campaign",
|
|
field_overrides: { department: false },
|
|
opt_ins: { inline_guidance: true }
|
|
}), "draft and merge metadata contains only client-owned fields");
|
|
const savePayload = {
|
|
editor_state: loadedEditorState,
|
|
base_revision: 7,
|
|
campaign_json: { template: { subject: "Updated template" } },
|
|
migrate_legacy_mail_settings: true
|
|
};
|
|
const requestPayload = campaignVersionUpdateForRequest(savePayload);
|
|
assert(JSON.stringify(requestPayload.editor_state) === JSON.stringify(clientState), "save, autosave, and fork requests omit review and approval evidence");
|
|
assert(!JSON.stringify(requestPayload).includes("legacy-secret"), "unknown legacy metadata is not resubmitted");
|
|
assert(requestPayload.base_revision === 7 && requestPayload.migrate_legacy_mail_settings, "concurrency and explicit migration controls are preserved");
|
|
assert(requestPayload.campaign_json === savePayload.campaign_json, "campaign content is not rewritten by metadata serialization");
|
|
assert(savePayload.editor_state.review_send.inspection_complete, "read response evidence is not mutated");
|
|
assert("approval_gate" in savePayload.editor_state, "read response approval evidence is preserved");
|
|
assert(Object.keys(clientCampaignEditorState(undefined)).length === 0, "new drafts need no metadata");
|
|
const omittedEditorState = { base_revision: 2, editor_state: undefined };
|
|
assert(campaignVersionUpdateForRequest(omittedEditorState) === omittedEditorState, "omitted metadata remains omitted");
|
|
const nullEditorState = { editor_state: null };
|
|
assert(campaignVersionUpdateForRequest(nullEditorState) === nullEditorState, "null metadata retains its unchanged-state semantics");
|