Ctrl K
ComunidadGuías…

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

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

Archivo original ↗Preguntar a KiBot
import { assertStringIncludes } from "@std/assert";
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import { hashEmail } from "#db/contact-preferences.ts";
import {
  buildBulkPayload,
  buildMailtoLink,
  resolveRecipientEmails,
} from "#shared/bulk-email.ts";
import {
  targetFromForm,
  targetFromQuery,
} from "#shared/bulk-email-targets/registry.ts";
import {
  resetEffectiveDomain,
  setEffectiveDomainForTest,
} from "#shared/config.ts";
import { BULK_UNSUBSCRIBE_PLACEHOLDER } from "#shared/email/bulk.ts";
import { FormParams } from "#shared/form-data.ts";
import { getTestPrivateKey } from "#test-utils/crypto.ts";
import { describeWithEnv } from "#test-utils/db.ts";
import { createTestAttendeeDirect } from "#test-utils/db-helpers/attendees.ts";
import {
  createTestListing,
  deactivateTestListing,
} from "#test-utils/db-helpers/listings.ts";

describe("mailto links", () => {
  test("buildMailtoLink addresses a lone recipient directly without BCC", () => {
    expect(buildMailtoLink(["a@b.com"], "Hi there", "Body & more")).toBe(
      "mailto:a%40b.com?subject=Hi%20there&body=Body%20%26%20more",
    );
  });

  test("buildMailtoLink addresses a lone recipient with a business email", () => {
    expect(buildMailtoLink(["a@b.com"], "Hi", "Body", "owner@biz.com")).toBe(
      "mailto:a%40b.com?subject=Hi&body=Body",
    );
  });

  test("buildMailtoLink BCCs several recipients from the business email", () => {
    expect(
      buildMailtoLink(
        ["a@b.com", "c@d.com"],
        "Hi there",
        "Body & more",
        "owner@biz.com",
      ),
    ).toBe(
      "mailto:owner%40biz.com?bcc=a%40b.com,c%40d.com&subject=Hi%20there&body=Body%20%26%20more",
    );
  });

  test("buildMailtoLink leaves To empty without a business email", () => {
    expect(
      buildMailtoLink(["a@b.com", "c@d.com"], "Hi there", "Body & more"),
    ).toBe(
      "mailto:?bcc=a%40b.com,c%40d.com&subject=Hi%20there&body=Body%20%26%20more",
    );
  });

  test("buildMailtoLink omits empty parts", () => {
    expect(buildMailtoLink([], "", "")).toBe("mailto:?");
  });

  test("buildMailtoLink encodes line breaks as a single %0A", () => {
    expect(
      buildMailtoLink([], "", "line one\r\nline two\rline three\nend"),
    ).toBe("mailto:?body=line%20one%0Aline%20two%0Aline%20three%0Aend");
  });
});

describeWithEnv("buildBulkPayload", { encryptionKey: true }, () => {
  test("transactional sends reach everyone with no footer", async () => {
    const payload = await buildBulkPayload({
      bodyHtml: "<p>Hi</p>",
      bodyText: "Hi",
      marketing: false,
      recipients: ["a@example.com", "b@example.com"],
      subject: "News",
      unsubscribed: new Set(),
    });
    expect(payload.html).toBe("<p>Hi</p>");
    expect(payload.subject).toBe("News");
    expect(payload.recipients).toEqual([
      { to: "a@example.com" },
      { to: "b@example.com" },
    ]);
  });

  test("marketing sends omit unsubscribed recipients", async () => {
    setEffectiveDomainForTest("tickets.example.com");
    const skipHash = await hashEmail("skip@example.com");
    try {
      const payload = await buildBulkPayload({
        bodyHtml: "<p>Hi</p>",
        bodyText: "Hi",
        marketing: true,
        recipients: ["keep@example.com", "skip@example.com"],
        subject: "Promo",
        unsubscribed: new Set([skipHash]),
      });
      const keepHash = await hashEmail("keep@example.com");
      const why =
        "You're receiving this because you registered for one of our listings.";
      assertStringIncludes(payload.html, why);
      assertStringIncludes(payload.text, why);
      assertStringIncludes(
        payload.html,
        `<a href="${BULK_UNSUBSCRIBE_PLACEHOLDER}">`,
      );
      assertStringIncludes(
        payload.text,
        `Unsubscribe or manage your preferences: ${BULK_UNSUBSCRIBE_PLACEHOLDER}`,
      );
      expect(payload.recipients).toEqual([
        {
          to: "keep@example.com",
          unsubscribeUrl: `https://tickets.example.com/unsubscribe?email=${encodeURIComponent(keepHash)}`,
        },
      ]);
    } finally {
      resetEffectiveDomain();
    }
  });
});

describeWithEnv("resolveRecipientEmails", { db: true }, () => {
  const setup = async () => {
    const active = await createTestListing({
      maxAttendees: 50,
      name: "Active",
    });
    await createTestAttendeeDirect(active.id, "Alice", "alice@example.com");
    await createTestAttendeeDirect(active.id, "Bob", "bob@example.com");
    const past = await createTestListing({
      date: "2020-06-01T10:00",
      maxAttendees: 50,
      name: "Past",
    });
    await createTestAttendeeDirect(past.id, "Dave", "dave@example.com");
    await createTestAttendeeDirect(past.id, "Alice", "alice@example.com");
    const inactive = await createTestListing({
      maxAttendees: 50,
      name: "Inactive",
    });
    await createTestAttendeeDirect(inactive.id, "Carol", "carol@example.com");
    await deactivateTestListing(inactive.id);
    return { active, inactive, past, pk: await getTestPrivateKey() };
  };

  test("all-attendees audience returns every de-duplicated address", async () => {
    const { pk } = await setup();
    expect(
      await resolveRecipientEmails({ audience: "all", kind: "audience" }, pk),
    ).toEqual([
      "alice@example.com",
      "bob@example.com",
      "carol@example.com",
      "dave@example.com",
    ]);
  });

  test("normalises stored addresses before it returns recipients", async () => {
    const { active, pk } = await setup();
    await createTestAttendeeDirect(active.id, "Duplicate", " Bob@Example.com ");
    await createTestAttendeeDirect(active.id, "Blank", "   ");

    expect(
      await resolveRecipientEmails(
        { kind: "listing", listingId: active.id },
        pk,
      ),
    ).toEqual(["alice@example.com", "bob@example.com"]);
  });

  test("active audience excludes deactivated listings", async () => {
    const { pk } = await setup();
    expect(
      await resolveRecipientEmails(
        { audience: "active", kind: "audience" },
        pk,
      ),
    ).toEqual(["alice@example.com", "bob@example.com", "dave@example.com"]);
  });

  test("upcoming audience excludes past and inactive listings", async () => {
    const { pk } = await setup();
    expect(
      await resolveRecipientEmails(
        { audience: "upcoming", kind: "audience" },
        pk,
      ),
    ).toEqual(["alice@example.com", "bob@example.com"]);
  });

  test("listing target returns only that listing's attendees", async () => {
    const { past, pk } = await setup();
    expect(
      await resolveRecipientEmails({ kind: "listing", listingId: past.id }, pk),
    ).toEqual(["alice@example.com", "dave@example.com"]);
  });

  test("attendee target returns just that attendee's address", async () => {
    const listing = await createTestListing({ maxAttendees: 5, name: "Solo" });
    const { token } = await createTestAttendeeDirect(
      listing.id,
      "Eve",
      "eve@example.com",
    );
    const pk = await getTestPrivateKey();
    expect(
      await resolveRecipientEmails({ kind: "attendee", token }, pk),
    ).toEqual(["eve@example.com"]);
  });

  test("unknown attendee token resolves to no recipients", async () => {
    const pk = await getTestPrivateKey();
    expect(
      await resolveRecipientEmails(
        { kind: "attendee", token: "does-not-exist" },
        pk,
      ),
    ).toEqual([]);
  });
});

describeWithEnv("bulk-email target parsing", { db: true }, () => {
  test("parses valid listing targets from query and form fields", async () => {
    const listing = await createTestListing({ name: "Mailout" });
    await createTestAttendeeDirect(listing.id, "Amy", "amy@example.com");
    await expect(
      targetFromQuery(new URLSearchParams({ listing: String(listing.id) })),
    ).resolves.toEqual({ kind: "listing", listingId: listing.id });
    await expect(
      targetFromForm(new FormParams({ listing_id: String(listing.id) })),
    ).resolves.toEqual({ kind: "listing", listingId: listing.id });
  });

  test("rejects malformed listing target ids", async () => {
    const listing = await createTestListing({ name: "Mailout" });
    const malformedId = `${listing.id}x`;
    await expect(
      targetFromQuery(new URLSearchParams({ listing: malformedId })),
    ).resolves.toBeNull();
    await expect(
      targetFromForm(new FormParams({ listing_id: malformedId })),
    ).resolves.toBeNull();
  });
});