Ctrl K
ComunidadGuías…

test/shared/bulk-email.test.ts

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

Archivo original ↗Preguntar a KiBot
import { assert, assertStringIncludes } from "@std/assert";
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import {
  resolveImageReferences,
  summarizeProviderResponse,
} from "#shared/bulk-email.ts";
import type {
  BatchMessageOutcome,
  BulkBatchResponse,
} from "#shared/email/bulk.ts";
import { expectIncludesAll } from "#test-utils/assertions.ts";

const TOOK_ALL: BatchMessageOutcome = {
  reasons: [],
  refused: 0,
  unconfirmed: 0,
};

/** One batch reply. A provider that took every message refuses none. */
const reply = (
  status: number,
  body: string,
  outcome: BatchMessageOutcome = TOOK_ALL,
): BulkBatchResponse => ({ body, ok: status < 400, outcome, status });

describe("summarizeProviderResponse", () => {
  test("notes when there were no responses at all", () => {
    expect(summarizeProviderResponse([])).toBe(
      "The email provider sent no response.",
    );
  });

  test("reports just the status when the body is empty", () => {
    expect(summarizeProviderResponse([reply(200, "")])).toBe(
      "The email provider responded with HTTP 200.",
    );
  });

  test("includes the provider's reply body when present", () => {
    expect(summarizeProviderResponse([reply(200, '{"id":"abc-123"}')])).toBe(
      'The email provider responded with HTTP 200: {"id":"abc-123"}.',
    );
  });

  test("surfaces a failed batch's status and reason", () => {
    expect(summarizeProviderResponse([reply(429, "rate limit exceeded")])).toBe(
      "The email provider responded with HTTP 429: rate limit exceeded.",
    );
  });

  test("de-duplicates identical replies across batches", () => {
    expect(
      summarizeProviderResponse([reply(200, "queued"), reply(200, "queued")]),
    ).toBe("The email provider responded with HTTP 200: queued.");
  });

  test("joins distinct per-batch replies", () => {
    expect(
      summarizeProviderResponse([reply(200, "queued"), reply(422, "rejected")]),
    ).toBe(
      "The email provider responded with HTTP 200: queued; HTTP 422: rejected.",
    );
  });

  test("truncates an over-long reply", () => {
    const long = "x".repeat(1000);
    const summary = summarizeProviderResponse([reply(200, long)]);
    assertStringIncludes(summary, "...");
    // Capped well below the raw body, which is never echoed in full.
    expect(summary.length).toBeLessThan(long.length);
    assert(!summary.includes(long));
  });

  test("names the messages an accepted batch refused anyway", () => {
    expect(
      summarizeProviderResponse([
        reply(200, "[]", {
          reasons: ["Postmark error 406: Inactive recipient"],
          refused: 2,
          unconfirmed: 0,
        }),
      ]),
    ).toBe(
      "The email provider responded with HTTP 200: []. It refused 2 messages." +
        " Postmark error 406: Inactive recipient.",
    );
  });

  test("counts one refused message in the singular", () => {
    assertStringIncludes(
      summarizeProviderResponse([
        reply(200, "[]", {
          reasons: ["Inactive recipient"],
          refused: 1,
          unconfirmed: 0,
        }),
      ]),
      "It refused 1 message.",
    );
  });

  test("still gives the refused count when no reason came back", () => {
    expect(
      summarizeProviderResponse([
        reply(429, "rate limit exceeded", {
          reasons: [],
          refused: 100,
          unconfirmed: 0,
        }),
      ]),
    ).toBe(
      "The email provider responded with HTTP 429: rate limit exceeded." +
        " It refused 100 messages.",
    );
  });

  test("adds the refused counts up across batches", () => {
    const refusal: BatchMessageOutcome = {
      reasons: ["Inactive recipient"],
      refused: 1,
      unconfirmed: 0,
    };
    expect(
      summarizeProviderResponse([
        reply(200, "[]", refusal),
        reply(200, "[]", refusal),
      ]),
    ).toBe(
      "The email provider responded with HTTP 200: []. It refused 2 messages." +
        " Inactive recipient.",
    );
  });

  test("warns about messages the provider did not confirm", () => {
    expect(
      summarizeProviderResponse([
        reply(200, "[]", { reasons: [], refused: 0, unconfirmed: 2 }),
      ]),
    ).toBe(
      "The email provider responded with HTTP 200: []." +
        " It did not confirm 2 messages. They may still have been sent." +
        " Check the provider before you send them again.",
    );
  });

  test("never calls an unconfirmed message a refusal", () => {
    const summary = summarizeProviderResponse([
      reply(200, "[]", { reasons: [], refused: 0, unconfirmed: 1 }),
    ]);

    assertStringIncludes(summary, "It did not confirm 1 message.");
    assert(!summary.includes("refused"));
  });

  test("separates two different refusal reasons", () => {
    assertStringIncludes(
      summarizeProviderResponse([
        reply(200, "[]", {
          reasons: ["Inactive recipient", "Hard bounce"],
          refused: 2,
          unconfirmed: 0,
        }),
      ]),
      "It refused 2 messages. Inactive recipient; Hard bounce.",
    );
  });

  test("caps an over-long list of refusal reasons", () => {
    const reasons = Array.from({ length: 40 }, (_, i) => `reason ${i} is long`);
    const summary = summarizeProviderResponse([
      reply(200, "[]", { reasons, refused: 40, unconfirmed: 0 }),
    ]);

    expectIncludesAll(summary, ["It refused 40 messages.", "..."]);
    assert(!summary.includes(reasons.join("; ")));
  });
});

describe("resolveImageReferences", () => {
  const urls = new Map([[5, "https://tickets.example/image/poster.webp"]]);

  test("replaces every reference the table holds with its address on the site", () => {
    expect(
      resolveImageReferences(
        "See ![Poster](img#5) and ![Poster](img#5).",
        urls,
      ),
    ).toBe(
      "See ![Poster](https://tickets.example/image/poster.webp) and ![Poster](https://tickets.example/image/poster.webp).",
    );
  });

  test("resolves a reference whose alt text escapes its brackets", () => {
    expect(resolveImageReferences("![Poster \\[blue\\]](img#5)", urls)).toBe(
      "![Poster \\[blue\\]](https://tickets.example/image/poster.webp)",
    );
  });

  test("resolves a titled reference and keeps its title", () => {
    expect(
      resolveImageReferences(
        'Poster: ![Poster](img#5 "our (best) poster")',
        urls,
      ),
    ).toBe(
      'Poster: ![Poster](https://tickets.example/image/poster.webp "our (best) poster")',
    );
  });

  test("resolves a reference-style picture and its definition", () => {
    expect(
      resolveImageReferences("![Poster][poster]\n\n[poster]: img#5", urls),
    ).toBe(
      "![Poster](https://tickets.example/image/poster.webp)\n\n[poster]: https://tickets.example/image/poster.webp",
    );
  });

  test("keeps a repeated definition line when it resolves the first one", () => {
    expect(
      resolveImageReferences("[5]: img#5\n\n[5]: img#7\n\n![A][5]", urls),
    ).toBe(
      "[5]: https://tickets.example/image/poster.webp\n\n[5]: img#7\n\n![A](https://tickets.example/image/poster.webp)",
    );
  });

  test("keeps the written form of a reference-style picture the table lacks", () => {
    expect(
      resolveImageReferences("![Poster][poster]\n\n[poster]: img#7", urls),
    ).toBe("![Poster][poster]\n\n[poster]: img#7");
  });

  test("keeps the written form of a reference the table lacks", () => {
    expect(resolveImageReferences("See ![Poster](img#7).", urls)).toBe(
      "See ![Poster](img#7).",
    );
  });

  test("keeps a web address that merely names an image", () => {
    expect(
      resolveImageReferences(
        "Directions: [the hall's page](https://example.org/img#5). See ![Poster](img#5).",
        urls,
      ),
    ).toBe(
      "Directions: [the hall's page](https://example.org/img#5). See ![Poster](https://tickets.example/image/poster.webp).",
    );
  });

  test("keeps the written form of a picture whose address is a web address", () => {
    expect(
      resolveImageReferences(
        "Poster: ![hall](https://files.example/hall.webp). See ![Poster](img#5).",
        urls,
      ),
    ).toBe(
      "Poster: ![hall](https://files.example/hall.webp). See ![Poster](https://tickets.example/image/poster.webp).",
    );
  });

  test("keeps prose that merely names an image", () => {
    expect(
      resolveImageReferences("Poster is picture img#5 on the wall.", urls),
    ).toBe("Poster is picture img#5 on the wall.");
  });

  test("keeps the written form when an escaped bracket closes the alt text", () => {
    expect(resolveImageReferences("![a\\](img#5)", urls)).toBe("![a\\](img#5)");
  });

  test("leaves a long run of backslashes after ![ alone", () => {
    const body = `![${"\\".repeat(10_000)}`;
    expect(resolveImageReferences(body, urls)).toBe(body);
  });

  test("keeps a reference inside a code span written as code", () => {
    expect(resolveImageReferences("`![Poster](img#5)`", urls)).toBe(
      "`![Poster](img#5)`",
    );
  });

  test("keeps a reference inside a fenced code block written as code", () => {
    const body = "```\n![Poster](img#5)\n```";
    expect(resolveImageReferences(body, urls)).toBe(body);
  });

  test("keeps code as code and resolves the picture beside it", () => {
    expect(
      resolveImageReferences(
        "Write `![Poster](img#5)` by hand: ![Poster](img#5).",
        urls,
      ),
    ).toBe(
      "Write `![Poster](img#5)` by hand: ![Poster](https://tickets.example/image/poster.webp).",
    );
  });
});