Ctrl K
ComunidadGuías…

test/shared/bulk-email/core.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import {
  type BulkEmailDraft,
  contactFrequencySummary,
  parseDraft,
  serializeDraft,
  validateDraftInput,
} from "#shared/bulk-email.ts";
import {
  targetComposeControl,
  targetQuery,
} from "#shared/bulk-email-targets/registry.ts";
import {
  AUDIENCES,
  audienceById,
  DEFAULT_AUDIENCE_ID,
  isAudienceId,
} from "#shared/bulk-email-targets/types.ts";
import { MAX_INPUT_LENGTH, MAX_TEXTAREA_LENGTH } from "#shared/limits.ts";

const audienceTarget = (audience: BulkEmailDraft["target"]) => audience;

describe("bulk-email audiences and targets", () => {
  test("AUDIENCES has a stable default that exists in the registry", () => {
    expect(isAudienceId(DEFAULT_AUDIENCE_ID)).toBe(true);
    expect(
      AUDIENCES.some((audience) => audience.id === DEFAULT_AUDIENCE_ID),
    ).toBe(true);
  });

  test("isAudienceId rejects unknown ids", () => {
    expect(isAudienceId("active")).toBe(true);
    expect(isAudienceId("nonsense")).toBe(false);
  });

  test("audienceById returns the matching definition", () => {
    expect(audienceById("all").label).toBe("All attendees");
  });

  test("targetQuery round-trips audience and listing targets", () => {
    expect(targetQuery({ audience: "upcoming", kind: "audience" })).toBe(
      "?audience=upcoming",
    );
    expect(targetQuery({ kind: "listing", listingId: 7 })).toBe("?listing=7");
  });

  test("targetQuery URL-encodes an attendee token", () => {
    expect(targetQuery({ kind: "attendee", token: "abc/def+ghi" })).toBe(
      "?attendee=abc%2Fdef%2Bghi",
    );
  });

  test("targetComposeControl drives the recipient control per kind", () => {
    const audience = targetComposeControl({
      audience: "active",
      kind: "audience",
    });
    expect(audience.mode).toBe("select");
    if (audience.mode === "select") {
      expect(audience.name).toBe("audience");
      expect(audience.selected).toBe("active");
      expect(audience.options.map((option) => option.value)).toEqual(
        AUDIENCES.map((entry) => entry.id),
      );
    }
    expect(targetComposeControl({ kind: "listing", listingId: 7 })).toEqual({
      fields: [["listing_id", "7"]],
      mode: "fixed",
    });
    expect(targetComposeControl({ kind: "attendee", token: "tok" })).toEqual({
      fields: [["attendee", "tok"]],
      mode: "fixed",
    });
  });
});

describe("bulk-email draft validation and serialization", () => {
  const target = audienceTarget({ audience: "active", kind: "audience" });

  test("accepts a valid draft", () => {
    const result = validateDraftInput({
      body: "Hello there",
      marketing: true,
      subject: "Subject",
      target,
    });
    expect(result.valid).toBe(true);
    if (result.valid) {
      expect(result.draft.subject).toBe("Subject");
      expect(result.draft.marketing).toBe(true);
    }
  });

  test("rejects an empty subject", () => {
    const result = validateDraftInput({
      body: "Body",
      marketing: false,
      subject: "   ",
      target,
    });
    expect(result).toEqual({ error: "Subject is required", valid: false });
  });

  test("rejects an over-length subject", () => {
    const result = validateDraftInput({
      body: "Body",
      marketing: false,
      subject: "x".repeat(MAX_INPUT_LENGTH + 1),
      target,
    });
    expect(result.valid).toBe(false);
  });

  test("accepts a subject at the shared single-line limit", () => {
    const draft = {
      body: "Body",
      marketing: false,
      subject: "x".repeat(MAX_INPUT_LENGTH),
      target,
    };
    expect(validateDraftInput(draft)).toEqual({ draft, valid: true });
  });

  test("rejects an empty body", () => {
    const result = validateDraftInput({
      body: "  ",
      marketing: false,
      subject: "Subject",
      target,
    });
    expect(result).toEqual({ error: "Message body is required", valid: false });
  });

  test("rejects an over-length body", () => {
    const result = validateDraftInput({
      body: "x".repeat(MAX_TEXTAREA_LENGTH + 1),
      marketing: false,
      subject: "Subject",
      target,
    });
    expect(result.valid).toBe(false);
  });

  test("serialize/parse round-trips", () => {
    const draft: BulkEmailDraft = {
      body: "Body",
      marketing: true,
      subject: "Subject",
      target,
    };
    expect(parseDraft(serializeDraft(draft))).toEqual(draft);
  });

  test("parseDraft returns null for empty, malformed, or invalid drafts", () => {
    expect(parseDraft("")).toBe(null);
    expect(parseDraft("{not json")).toBe(null);
    expect(parseDraft(JSON.stringify({ subject: 1 }))).toBe(null);
    expect(
      parseDraft(
        JSON.stringify({
          body: "b",
          marketing: false,
          subject: "s",
          target: { audience: "bogus", kind: "audience" },
        }),
      ),
    ).toBe(null);
  });
});

describe("contactFrequencySummary", () => {
  test("is empty with no recipients", () => {
    expect(contactFrequencySummary([])).toBe("");
  });

  test("reports never-contacted when all counts are zero", () => {
    expect(contactFrequencySummary([0, 0, 0])).toBe(
      "These attendees have never been contacted through this page.",
    );
  });

  test("reports a whole number when the average is an integer", () => {
    expect(contactFrequencySummary([2, 2, 2])).toBe(
      "These attendees have been contacted through this page 2 times each.",
    );
  });

  test("reports a one-decimal average otherwise", () => {
    expect(contactFrequencySummary([1, 2])).toBe(
      "These attendees have been contacted through this page an average of 1.5 times each.",
    );
  });
});