Files
suaveplan/packages/api-stack/api/api-plugin-campaigns/src/routes/campaigns/schemas.test.ts
2026-05-27 23:29:53 +01:00

147 lines
4.7 KiB
TypeScript

import { describe, expect, it } from "@suaveplan/testing/runner";
import {
CampaignIdParamSchema,
CampaignListResponseSchema,
CampaignQuerySchema,
CampaignResponseSchema,
CampaignStatusSchema,
CampaignTypeSchema,
CreateCampaignSchema,
EMPTY_STATS,
UpdateCampaignSchema,
} from "./schemas.js";
describe("campaign route schemas", () => {
it("CampaignStatusSchema accepts all 7 statuses", { timeout: 5000 }, () => {
for (const s of [
"draft",
"scheduled",
"sending",
"paused",
"completed",
"cancelled",
"failed",
] as const) {
expect(CampaignStatusSchema.parse(s)).toBe(s);
}
expect(() => CampaignStatusSchema.parse("bogus")).toThrow();
});
it("CampaignTypeSchema accepts all 4 types", { timeout: 5000 }, () => {
for (const t of [
"broadcast",
"triggered",
"ab_test",
"drip",
] as const) {
expect(CampaignTypeSchema.parse(t)).toBe(t);
}
expect(() => CampaignTypeSchema.parse("bogus")).toThrow();
});
it("CampaignIdParamSchema rejects empty id", { timeout: 5000 }, () => {
expect(() => CampaignIdParamSchema.parse({ id: "" })).toThrow();
expect(CampaignIdParamSchema.parse({ id: "a" })).toEqual({ id: "a" });
});
it("CreateCampaignSchema applies all documented defaults", {
timeout: 5000,
}, () => {
const parsed = CreateCampaignSchema.parse({ name: "x" });
expect(parsed.type).toBe("broadcast");
expect(parsed.channel).toBe("email");
expect(parsed.audienceConfig).toEqual({ groupIds: [] });
expect(parsed.templateData).toEqual({});
expect(parsed.metadata).toEqual({});
});
it("CreateCampaignSchema rejects empty name", { timeout: 5000 }, () => {
expect(() => CreateCampaignSchema.parse({ name: "" })).toThrow();
});
it("CreateCampaignSchema accepts a full body", { timeout: 5000 }, () => {
const out = CreateCampaignSchema.parse({
name: "Promo",
type: "drip",
channel: "sms",
audienceConfig: {
groupIds: ["g1"],
excludeGroupIds: ["g2"],
},
templateId: "tpl1",
templateData: { foo: 1 },
schedule: { type: "immediate" },
throttle: { messagesPerSecond: 5 },
abTestConfig: { splitType: "even" },
metadata: { campaignId: "X" },
});
expect(out.type).toBe("drip");
expect(out.audienceConfig.groupIds).toEqual(["g1"]);
});
it("UpdateCampaignSchema accepts a partial body", { timeout: 5000 }, () => {
const out = UpdateCampaignSchema.parse({ name: "New" });
expect(out.name).toBe("New");
});
it("UpdateCampaignSchema rejects empty name", { timeout: 5000 }, () => {
expect(() => UpdateCampaignSchema.parse({ name: "" })).toThrow();
});
it("CampaignQuerySchema coerces query strings and applies defaults", {
timeout: 5000,
}, () => {
const out = CampaignQuerySchema.parse({
status: "draft",
type: "broadcast",
limit: "25",
offset: "5",
});
expect(out.limit).toBe(25);
expect(out.offset).toBe(5);
expect(out.status).toBe("draft");
});
it("CampaignQuerySchema clamps limit between 1 and 200", {
timeout: 5000,
}, () => {
expect(() => CampaignQuerySchema.parse({ limit: 0 })).toThrow();
expect(() => CampaignQuerySchema.parse({ limit: 300 })).toThrow();
});
it("CampaignResponseSchema accepts a minimally valid response", {
timeout: 5000,
}, () => {
const ok = CampaignResponseSchema.safeParse({
id: "1",
tenantId: "t",
name: "x",
status: "draft",
type: "broadcast",
channel: "email",
audienceConfig: { groupIds: [] },
templateData: {},
metadata: {},
stats: EMPTY_STATS,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
expect(ok.success).toBe(true);
});
it("CampaignListResponseSchema wraps the campaign array", {
timeout: 5000,
}, () => {
const ok = CampaignListResponseSchema.safeParse({
campaigns: [],
total: 0,
});
expect(ok.success).toBe(true);
});
it("EMPTY_STATS exposes every zero counter", { timeout: 5000 }, () => {
expect(Object.values(EMPTY_STATS).every((v) => v === 0)).toBe(true);
expect(Object.keys(EMPTY_STATS)).toHaveLength(9);
});
});