feat: add campaign configuration controls
This commit is contained in:
265
tests/campaign-form.test.ts
Normal file
265
tests/campaign-form.test.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import * as campaignForm from "../lib/campaign-form";
|
||||
|
||||
type CampaignFormModule = {
|
||||
campaignFormSchema: {
|
||||
safeParse: (value: unknown) => {
|
||||
success: boolean;
|
||||
data?: Record<string, unknown>;
|
||||
error?: {
|
||||
flatten?: () => { fieldErrors: Record<string, string[] | undefined> };
|
||||
issues?: Array<{
|
||||
path?: Array<string | number>;
|
||||
message?: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
mapCampaignFormToPayload: (values: Record<string, unknown>) => Record<string, unknown>;
|
||||
};
|
||||
|
||||
type ErrorLike = CampaignFormModule["campaignFormSchema"]["safeParse"] extends (
|
||||
value: unknown,
|
||||
) => infer Result
|
||||
? Result
|
||||
: never;
|
||||
|
||||
const formModule = campaignForm as unknown as CampaignFormModule;
|
||||
|
||||
function toString(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function extractFieldMessages(
|
||||
result: ErrorLike,
|
||||
field: string,
|
||||
): string[] {
|
||||
const messages: string[] = [];
|
||||
const flatten = result.error?.flatten?.();
|
||||
const flattenedMessages = flatten?.fieldErrors?.[field];
|
||||
|
||||
if (flattenedMessages && Array.isArray(flattenedMessages)) {
|
||||
for (const message of flattenedMessages) {
|
||||
if (typeof message === "string" && message.length > 0) {
|
||||
messages.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const issue of result.error?.issues ?? []) {
|
||||
const isTargetField = issue.path?.at(-1) === field;
|
||||
if (
|
||||
isTargetField &&
|
||||
typeof issue.message === "string" &&
|
||||
issue.message.length > 0
|
||||
) {
|
||||
messages.push(issue.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
const fallback = JSON.stringify(result.error);
|
||||
if (fallback.length > 2) {
|
||||
messages.push(fallback);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
function createMinimalValidForm(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
name: "Malerbetrieb Konstruktiv",
|
||||
categoryMode: "preset",
|
||||
category: "Maler",
|
||||
customSearchTerm: "",
|
||||
postalCode: "79098",
|
||||
radiusKm: 10,
|
||||
maxNewLeadsPerRun: 5,
|
||||
maxAuditsPerRun: 5,
|
||||
recurrence: "daily",
|
||||
status: "active",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("valid minimal campaign form maps to a Germany-only payload", () => {
|
||||
const schemaResult = formModule.campaignFormSchema.safeParse(
|
||||
createMinimalValidForm(),
|
||||
);
|
||||
|
||||
assert.equal(schemaResult.success, true);
|
||||
const payload = formModule.mapCampaignFormToPayload(
|
||||
schemaResult.data as Record<string, unknown>,
|
||||
) as Record<string, unknown>;
|
||||
const germanyIndicators = [
|
||||
payload.countryCode,
|
||||
payload.country,
|
||||
payload.region,
|
||||
];
|
||||
|
||||
const hasGermany = germanyIndicators.some((value) => {
|
||||
const normalized = toString(value)?.toLowerCase();
|
||||
return (
|
||||
normalized === "de" ||
|
||||
normalized === "deutschland" ||
|
||||
normalized?.includes("deutschland")
|
||||
);
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
hasGermany,
|
||||
true,
|
||||
"Mapped payload should enforce Germany-only context",
|
||||
);
|
||||
});
|
||||
|
||||
test("German PLZ validation enforces exactly five digits with clear German feedback", () => {
|
||||
const schema = formModule.campaignFormSchema;
|
||||
const invalidPostcodes = [
|
||||
"",
|
||||
"1234",
|
||||
"123456",
|
||||
"abcde",
|
||||
"12 34",
|
||||
];
|
||||
|
||||
for (const postalCode of invalidPostcodes) {
|
||||
const result = schema.safeParse(createMinimalValidForm({ postalCode }));
|
||||
|
||||
assert.equal(result.success, false);
|
||||
const messages = extractFieldMessages(result as ErrorLike, "postalCode");
|
||||
assert.ok(messages.length > 0, "Expected a validation message for postal code");
|
||||
|
||||
const messageText = messages.join(" ");
|
||||
assert.match(
|
||||
messageText,
|
||||
/PLZ|Postleitzahl/i,
|
||||
`Expected postal-code message in German semantics, got: ${messageText}`,
|
||||
);
|
||||
assert.match(
|
||||
messageText,
|
||||
/5|fünf|5-stellig|stellig/i,
|
||||
`Expected explicit five-digit guidance, got: ${messageText}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("predefined categories do not require custom niche input", () => {
|
||||
const preset = createMinimalValidForm({
|
||||
categoryMode: "preset",
|
||||
category: "Maler",
|
||||
customSearchTerm: "",
|
||||
});
|
||||
|
||||
const result = formModule.campaignFormSchema.safeParse(preset);
|
||||
|
||||
assert.equal(result.success, true, "Predefined category should parse without custom term");
|
||||
});
|
||||
|
||||
test("Anderes/custom category requires a non-empty custom niche field", () => {
|
||||
const anderes = formModule.campaignFormSchema.safeParse(
|
||||
createMinimalValidForm({
|
||||
categoryMode: "custom",
|
||||
category: "Anderes",
|
||||
customSearchTerm: "",
|
||||
}),
|
||||
);
|
||||
assert.equal(anderes.success, false);
|
||||
const messages = extractFieldMessages(anderes as ErrorLike, "customSearchTerm");
|
||||
assert.ok(messages.length > 0, "Expected a validation message for missing custom term");
|
||||
|
||||
const messageText = messages.join(" ");
|
||||
assert.match(
|
||||
messageText,
|
||||
/Anderes|eigene|benötigt|erforderlich|muss/i,
|
||||
`Expected explicit guidance for Anderes custom term, got: ${messageText}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("radius and lead/audit limits reject invalid zero, negative, and out-of-range values", () => {
|
||||
const schema = formModule.campaignFormSchema;
|
||||
const fieldLimits: Array<{ field: "radiusKm" | "maxNewLeadsPerRun" | "maxAuditsPerRun"; invalid: number[] }> = [
|
||||
{ field: "radiusKm", invalid: [0, -1, 10000] },
|
||||
{ field: "maxNewLeadsPerRun", invalid: [0, -1, 10000] },
|
||||
{ field: "maxAuditsPerRun", invalid: [0, -1, 10000] },
|
||||
];
|
||||
|
||||
for (const { field, invalid } of fieldLimits) {
|
||||
for (const value of invalid) {
|
||||
const values = createMinimalValidForm({ [field]: value });
|
||||
const result = schema.safeParse(values);
|
||||
|
||||
assert.equal(result.success, false);
|
||||
const messages = extractFieldMessages(result as ErrorLike, field);
|
||||
assert.ok(
|
||||
messages.length > 0,
|
||||
`Expected validation for invalid value on ${field}: ${value}`,
|
||||
);
|
||||
|
||||
const messageText = messages.join(" ");
|
||||
assert.match(
|
||||
messageText,
|
||||
/muss|mindestens|zwischen|gültig|positiv/i,
|
||||
`Expected German limit/range explanation for ${field}, got: ${messageText}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("radius and lead/audit limits reject decimal values", () => {
|
||||
const schema = formModule.campaignFormSchema;
|
||||
const fieldLimits: Array<{ field: "radiusKm" | "maxNewLeadsPerRun" | "maxAuditsPerRun"; invalid: number[] }> = [
|
||||
{ field: "radiusKm", invalid: [10.5, 0.1, 12.99] },
|
||||
{ field: "maxNewLeadsPerRun", invalid: [1.9, 5.25] },
|
||||
{ field: "maxAuditsPerRun", invalid: [0.01, 2.75] },
|
||||
];
|
||||
|
||||
for (const { field, invalid } of fieldLimits) {
|
||||
for (const value of invalid) {
|
||||
const values = createMinimalValidForm({ [field]: value });
|
||||
const result = schema.safeParse(values);
|
||||
|
||||
assert.equal(result.success, false);
|
||||
const messages = extractFieldMessages(result as ErrorLike, field);
|
||||
assert.ok(
|
||||
messages.length > 0,
|
||||
`Expected validation for decimal value on ${field}: ${value}`,
|
||||
);
|
||||
|
||||
const messageText = messages.join(" ");
|
||||
assert.match(
|
||||
messageText,
|
||||
/Ganzzahl|ganze|integer|Nachkommastellen|dezim|komma/i,
|
||||
`Expected decimal-rejection guidance for ${field}, got: ${messageText}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("recurrence/cadence only accepts manual, daily, weekly, monthly", () => {
|
||||
const schema = formModule.campaignFormSchema;
|
||||
const validValues = ["manual", "daily", "weekly", "monthly"] as const;
|
||||
const invalidValues = ["hourly", "biweekly", "yearly", ""] as const;
|
||||
|
||||
for (const recurrence of validValues) {
|
||||
const valid = schema.safeParse(createMinimalValidForm({ recurrence }));
|
||||
assert.equal(valid.success, true, `Expected recurrence ${recurrence} to parse`);
|
||||
}
|
||||
|
||||
for (const recurrence of invalidValues) {
|
||||
const invalid = schema.safeParse(createMinimalValidForm({ recurrence }));
|
||||
assert.equal(invalid.success, false, `Expected recurrence ${recurrence} to be rejected`);
|
||||
|
||||
const messages = extractFieldMessages(invalid as ErrorLike, "recurrence");
|
||||
assert.ok(messages.length > 0);
|
||||
const messageText = messages.join(" ");
|
||||
assert.match(
|
||||
messageText,
|
||||
/manuell|täglich|wöchentlich|monatlich|Auswahl|gültig/i,
|
||||
`Expected actionable German cadence guidance, got: ${messageText}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
270
tests/campaign-scheduling.test.ts
Normal file
270
tests/campaign-scheduling.test.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import * as campaignScheduling from "../lib/campaign-scheduling";
|
||||
|
||||
type CampaignSchedulingModule = {
|
||||
calculateNextRunAt: (input: {
|
||||
recurrence: string;
|
||||
status: "active" | "paused";
|
||||
lastRunAt?: number | null;
|
||||
now?: number;
|
||||
}) => number | null;
|
||||
getCampaignCurrentRunStatus: (
|
||||
input: {
|
||||
campaignStatus: "active" | "paused";
|
||||
agentRuns?: Array<{
|
||||
status: string;
|
||||
updatedAt?: number;
|
||||
}>;
|
||||
},
|
||||
) => string;
|
||||
isAllowedCampaignRecurrence?: (value: string) => boolean;
|
||||
CAMPAIGN_RECURRENCES?: readonly string[];
|
||||
};
|
||||
|
||||
type SchedulingRun = {
|
||||
status: string;
|
||||
updatedAt?: number;
|
||||
};
|
||||
|
||||
const schedulingModule = campaignScheduling as unknown as CampaignSchedulingModule;
|
||||
|
||||
function addDaysUTC(timestamp: number, days: number) {
|
||||
const date = new Date(timestamp);
|
||||
return Date.UTC(
|
||||
date.getUTCFullYear(),
|
||||
date.getUTCMonth(),
|
||||
date.getUTCDate() + days,
|
||||
date.getUTCHours(),
|
||||
date.getUTCMinutes(),
|
||||
date.getUTCSeconds(),
|
||||
date.getUTCMilliseconds(),
|
||||
);
|
||||
}
|
||||
|
||||
function addMonthsUTC(timestamp: number, months: number) {
|
||||
const date = new Date(timestamp);
|
||||
return Date.UTC(
|
||||
date.getUTCFullYear(),
|
||||
date.getUTCMonth() + months,
|
||||
date.getUTCDate(),
|
||||
date.getUTCHours(),
|
||||
date.getUTCMinutes(),
|
||||
date.getUTCSeconds(),
|
||||
date.getUTCMilliseconds(),
|
||||
);
|
||||
}
|
||||
|
||||
function expectFunction<T>(value: unknown, name: string): T {
|
||||
assert.equal(
|
||||
typeof value,
|
||||
"function",
|
||||
`Expected ${name} to be implemented and exported`,
|
||||
);
|
||||
return value as T;
|
||||
}
|
||||
|
||||
function runWithLatest(updatedRuns: SchedulingRun[]) {
|
||||
return updatedRuns.sort((a, b) => {
|
||||
const aTime = typeof a.updatedAt === "number" ? a.updatedAt : 0;
|
||||
const bTime = typeof b.updatedAt === "number" ? b.updatedAt : 0;
|
||||
return bTime - aTime;
|
||||
});
|
||||
}
|
||||
|
||||
test("recurrence validation only allows manual/daily/weekly/monthly", () => {
|
||||
const allowed = ["manual", "daily", "weekly", "monthly"];
|
||||
const forbidden = ["hourly", "2xweekly", "biweekly", ""];
|
||||
|
||||
if (typeof schedulingModule.isAllowedCampaignRecurrence === "function") {
|
||||
for (const value of allowed) {
|
||||
assert.equal(
|
||||
schedulingModule.isAllowedCampaignRecurrence(value),
|
||||
true,
|
||||
`${value} should be accepted`,
|
||||
);
|
||||
}
|
||||
for (const value of forbidden) {
|
||||
assert.equal(
|
||||
schedulingModule.isAllowedCampaignRecurrence(value),
|
||||
false,
|
||||
`${value} should be rejected`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
Array.isArray(schedulingModule.CAMPAIGN_RECURRENCES),
|
||||
"Expected recurrence validation helper or CAMPAIGN_RECURRENCES list",
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...schedulingModule.CAMPAIGN_RECURRENCES!].sort(),
|
||||
[...allowed].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
test("next run is null for manual or paused campaigns and scheduled for active recurring campaigns", () => {
|
||||
const calculateNextRunAt = expectFunction<(input: {
|
||||
recurrence: string;
|
||||
status: "active" | "paused";
|
||||
lastRunAt?: number | null;
|
||||
now?: number;
|
||||
}) => number | null>(schedulingModule.calculateNextRunAt, "calculateNextRunAt");
|
||||
|
||||
const lastRunAt = Date.UTC(2026, 5, 1, 8, 0, 0);
|
||||
|
||||
assert.equal(
|
||||
calculateNextRunAt({
|
||||
recurrence: "manual",
|
||||
status: "active",
|
||||
lastRunAt,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
calculateNextRunAt({
|
||||
recurrence: "daily",
|
||||
status: "paused",
|
||||
lastRunAt,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
|
||||
const dailyNext = calculateNextRunAt({
|
||||
recurrence: "daily",
|
||||
status: "active",
|
||||
lastRunAt,
|
||||
});
|
||||
const weeklyNext = calculateNextRunAt({
|
||||
recurrence: "weekly",
|
||||
status: "active",
|
||||
lastRunAt,
|
||||
});
|
||||
const monthlyNext = calculateNextRunAt({
|
||||
recurrence: "monthly",
|
||||
status: "active",
|
||||
lastRunAt,
|
||||
});
|
||||
|
||||
assert.equal(dailyNext, addDaysUTC(lastRunAt, 1));
|
||||
assert.equal(weeklyNext, addDaysUTC(lastRunAt, 7));
|
||||
assert.equal(monthlyNext, addMonthsUTC(lastRunAt, 1));
|
||||
});
|
||||
|
||||
test("run status favors running/pending over finished states", () => {
|
||||
const getCampaignCurrentRunStatus = expectFunction<
|
||||
(input: {
|
||||
campaignStatus: "active" | "paused";
|
||||
agentRuns?: Array<{
|
||||
status: string;
|
||||
updatedAt?: number;
|
||||
}>;
|
||||
}) => string
|
||||
>(schedulingModule.getCampaignCurrentRunStatus, "getCampaignCurrentRunStatus");
|
||||
|
||||
const activeWithRunningAndFinished = runWithLatest([
|
||||
{ status: "succeeded", updatedAt: Date.UTC(2026, 5, 2, 10, 0, 0) },
|
||||
{ status: "running", updatedAt: Date.UTC(2026, 5, 2, 10, 5, 0) },
|
||||
{ status: "failed", updatedAt: Date.UTC(2026, 5, 2, 9, 50, 0) },
|
||||
]);
|
||||
|
||||
const runningStatus = getCampaignCurrentRunStatus({
|
||||
campaignStatus: "active",
|
||||
agentRuns: activeWithRunningAndFinished,
|
||||
});
|
||||
assert.equal(
|
||||
runningStatus,
|
||||
"running",
|
||||
"Active running campaign should surface running as current status",
|
||||
);
|
||||
|
||||
const pendingOverFinished = runWithLatest([
|
||||
{ status: "succeeded", updatedAt: Date.UTC(2026, 5, 2, 10, 0, 0) },
|
||||
{ status: "pending", updatedAt: Date.UTC(2026, 5, 2, 10, 5, 0) },
|
||||
{ status: "failed", updatedAt: Date.UTC(2026, 5, 2, 9, 50, 0) },
|
||||
]);
|
||||
const pendingStatus = getCampaignCurrentRunStatus({
|
||||
campaignStatus: "active",
|
||||
agentRuns: pendingOverFinished,
|
||||
});
|
||||
assert.equal(
|
||||
pendingStatus,
|
||||
"pending",
|
||||
"Pending should outrank finished statuses when no running is active",
|
||||
);
|
||||
|
||||
const pausedWithoutRuns = getCampaignCurrentRunStatus({
|
||||
campaignStatus: "paused",
|
||||
agentRuns: [],
|
||||
});
|
||||
assert.equal(
|
||||
pausedWithoutRuns,
|
||||
"paused",
|
||||
"Paused campaigns should not report campaign activity by default",
|
||||
);
|
||||
});
|
||||
|
||||
test("run status uses latest run first (running, pending, otherwise terminal)", () => {
|
||||
const getCampaignCurrentRunStatus = expectFunction<
|
||||
(input: {
|
||||
campaignStatus: "active" | "paused";
|
||||
agentRuns?: Array<{
|
||||
status: string;
|
||||
updatedAt?: number;
|
||||
}>;
|
||||
}) => string
|
||||
>(schedulingModule.getCampaignCurrentRunStatus, "getCampaignCurrentRunStatus");
|
||||
|
||||
const activeWithoutRuns = getCampaignCurrentRunStatus({
|
||||
campaignStatus: "active",
|
||||
agentRuns: [],
|
||||
});
|
||||
assert.equal(activeWithoutRuns, "idle");
|
||||
|
||||
const unsortedRuns = [
|
||||
{ status: "failed", updatedAt: Date.UTC(2026, 5, 1, 9, 0, 0) },
|
||||
{ status: "running", updatedAt: Date.UTC(2026, 5, 1, 8, 0, 0) },
|
||||
{ status: "failed", updatedAt: Date.UTC(2026, 5, 1, 7, 0, 0) },
|
||||
];
|
||||
const latestRunWins = getCampaignCurrentRunStatus({
|
||||
campaignStatus: "active",
|
||||
agentRuns: unsortedRuns,
|
||||
});
|
||||
assert.equal(
|
||||
latestRunWins,
|
||||
"failed",
|
||||
"Latest status should determine current status, not any older run",
|
||||
);
|
||||
|
||||
const unsortedPending = [
|
||||
{ status: "running", updatedAt: Date.UTC(2026, 5, 1, 9, 0, 0) },
|
||||
{ status: "pending", updatedAt: Date.UTC(2026, 5, 1, 9, 5, 0) },
|
||||
{ status: "succeeded", updatedAt: Date.UTC(2026, 5, 1, 7, 0, 0) },
|
||||
];
|
||||
const latestPending = getCampaignCurrentRunStatus({
|
||||
campaignStatus: "active",
|
||||
agentRuns: unsortedPending,
|
||||
});
|
||||
assert.equal(
|
||||
latestPending,
|
||||
"pending",
|
||||
"Pending latest run should be surfaced when it is the most recent.",
|
||||
);
|
||||
|
||||
const unsortedRunning = [
|
||||
{ status: "succeeded", updatedAt: Date.UTC(2026, 5, 1, 9, 0, 0) },
|
||||
{ status: "running", updatedAt: Date.UTC(2026, 5, 1, 10, 5, 0) },
|
||||
{ status: "pending", updatedAt: Date.UTC(2026, 5, 1, 8, 0, 0) },
|
||||
];
|
||||
const latestRunning = getCampaignCurrentRunStatus({
|
||||
campaignStatus: "active",
|
||||
agentRuns: unsortedRunning,
|
||||
});
|
||||
assert.equal(
|
||||
latestRunning,
|
||||
"running",
|
||||
"Running should be surfaced when it is the latest run.",
|
||||
);
|
||||
});
|
||||
100
tests/campaign-validation.test.ts
Normal file
100
tests/campaign-validation.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
validateCampaignCreateInput,
|
||||
validateCampaignUpdateInput,
|
||||
} from "../lib/campaign-validation";
|
||||
|
||||
test("campaign mutation validation normalizes and enforces fixed Germany context", () => {
|
||||
const payload = validateCampaignCreateInput({
|
||||
status: "active",
|
||||
recurrence: "daily",
|
||||
postalCode: "10115",
|
||||
radiusKm: 10,
|
||||
maxNewLeadsPerRun: 5,
|
||||
maxAuditsPerRun: 5,
|
||||
});
|
||||
|
||||
assert.equal(payload.countryCode, "DE");
|
||||
assert.equal(payload.country, "Deutschland");
|
||||
});
|
||||
|
||||
test("campaign validation rejects invalid German PLZ", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
validateCampaignCreateInput({
|
||||
status: "active",
|
||||
recurrence: "daily",
|
||||
postalCode: "1234",
|
||||
radiusKm: 10,
|
||||
maxNewLeadsPerRun: 5,
|
||||
maxAuditsPerRun: 5,
|
||||
}),
|
||||
(error: unknown) => {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
error.message.includes("5") &&
|
||||
/PLZ|Postleitzahl/i.test(error.message)
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("campaign validation rejects decimal limits", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
validateCampaignCreateInput({
|
||||
status: "active",
|
||||
recurrence: "daily",
|
||||
postalCode: "10115",
|
||||
radiusKm: 10.5,
|
||||
maxNewLeadsPerRun: 5,
|
||||
maxAuditsPerRun: 5,
|
||||
}),
|
||||
(error: unknown) => {
|
||||
return error instanceof Error && error.message.includes("ganze");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("campaign validation rejects invalid recurrence/status in German", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
validateCampaignCreateInput({
|
||||
status: "running",
|
||||
recurrence: "daily",
|
||||
postalCode: "10115",
|
||||
radiusKm: 10,
|
||||
maxNewLeadsPerRun: 5,
|
||||
maxAuditsPerRun: 5,
|
||||
}),
|
||||
(error: unknown) => error instanceof Error && error.message.includes("Status"),
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
validateCampaignCreateInput({
|
||||
status: "active",
|
||||
recurrence: "hourly",
|
||||
postalCode: "10115",
|
||||
radiusKm: 10,
|
||||
maxNewLeadsPerRun: 5,
|
||||
maxAuditsPerRun: 5,
|
||||
}),
|
||||
(error: unknown) =>
|
||||
error instanceof Error && /Frequenz|ungültig/.test(error.message),
|
||||
);
|
||||
});
|
||||
|
||||
test("campaign update validation rejects partial Germany-context payloads", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
validateCampaignUpdateInput({
|
||||
countryCode: "DE",
|
||||
}),
|
||||
(error: unknown) =>
|
||||
error instanceof Error &&
|
||||
/vollständig|Deutschland-Kontext/.test(error.message),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user