mailery 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/testing.js CHANGED
@@ -491,22 +491,22 @@ function __awaiter(thisArg, _arguments, P, generator) {
491
491
  return new (P || (P = Promise))(function(resolve, reject) {
492
492
  function fulfilled(value) {
493
493
  try {
494
- step(generator.next(value));
494
+ step2(generator.next(value));
495
495
  } catch (e) {
496
496
  reject(e);
497
497
  }
498
498
  }
499
499
  function rejected(value) {
500
500
  try {
501
- step(generator["throw"](value));
501
+ step2(generator["throw"](value));
502
502
  } catch (e) {
503
503
  reject(e);
504
504
  }
505
505
  }
506
- function step(result) {
506
+ function step2(result) {
507
507
  result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
508
508
  }
509
- step((generator = generator.apply(thisArg, _arguments || [])).next());
509
+ step2((generator = generator.apply(thisArg, _arguments || [])).next());
510
510
  });
511
511
  }
512
512
  function __generator(thisArg, body) {
@@ -519,10 +519,10 @@ function __generator(thisArg, body) {
519
519
  }), g;
520
520
  function verb(n) {
521
521
  return function(v) {
522
- return step([n, v]);
522
+ return step2([n, v]);
523
523
  };
524
524
  }
525
- function step(op) {
525
+ function step2(op) {
526
526
  if (f) throw new TypeError("Generator is already executing.");
527
527
  while (g && (g = 0, op[0] && (_ = 0)), _) try {
528
528
  if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
@@ -656,12 +656,12 @@ function __asyncGenerator(thisArg, _arguments, generator) {
656
656
  }
657
657
  function resume(n, v) {
658
658
  try {
659
- step(g[n](v));
659
+ step2(g[n](v));
660
660
  } catch (e) {
661
661
  settle(q[0][3], e);
662
662
  }
663
663
  }
664
- function step(r) {
664
+ function step2(r) {
665
665
  r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r);
666
666
  }
667
667
  function fulfill(value) {
@@ -7079,9 +7079,9 @@ var require_pack = __commonJS({
7079
7079
  this.push(headers.encode(newHeader));
7080
7080
  }
7081
7081
  _doDrain() {
7082
- const drain = this._drain;
7082
+ const drain2 = this._drain;
7083
7083
  this._drain = noop;
7084
- drain();
7084
+ drain2();
7085
7085
  }
7086
7086
  _predestroy() {
7087
7087
  const err = getStreamError(this);
@@ -15472,10 +15472,10 @@ async function getBucketStatus(ctx, fromEmail, kind) {
15472
15472
  }
15473
15473
 
15474
15474
  // src/server/runner/send.ts
15475
- async function handleSend(run, step, contact, flow, ctx) {
15476
- const template = await ctx.collections.templates.findOne({ slug: step.templateSlug });
15475
+ async function handleSend(run, step2, contact, flow, ctx) {
15476
+ const template = await ctx.collections.templates.findOne({ slug: step2.templateSlug });
15477
15477
  if (!template) {
15478
- await failFlowRun(run, `template not found: ${step.templateSlug}`, ctx);
15478
+ await failFlowRun(run, `template not found: ${step2.templateSlug}`, ctx);
15479
15479
  return;
15480
15480
  }
15481
15481
  const dedupeKey = `flowrun:${run._id}:step${run.currentStepIndex}`;
@@ -15487,8 +15487,8 @@ async function handleSend(run, step, contact, flow, ctx) {
15487
15487
  });
15488
15488
  return;
15489
15489
  }
15490
- const providerName = pickProviderName(step.providerOverride, template, ctx);
15491
- const renderCtx = buildRenderContext(contact, run, step.vars ?? {}, ctx);
15490
+ const providerName = pickProviderName(step2.providerOverride, template, ctx);
15491
+ const renderCtx = buildRenderContext(contact, run, step2.vars ?? {}, ctx);
15492
15492
  let rendered;
15493
15493
  try {
15494
15494
  rendered = await renderTemplate(template, renderCtx, { helpers: ctx.handlebarsHelpers });
@@ -15525,7 +15525,7 @@ async function handleSend(run, step, contact, flow, ctx) {
15525
15525
  bodyHash: sha256(rendered.html),
15526
15526
  status: "queued",
15527
15527
  renderedFrom: rendered.fromName ? { name: rendered.fromName, email: rendered.fromEmail } : void 0,
15528
- vars: step.vars ?? {}
15528
+ vars: step2.vars ?? {}
15529
15529
  })
15530
15530
  );
15531
15531
  await ctx.queues.send.add(
@@ -15614,6 +15614,10 @@ async function dispatchSend(sendId, ctx) {
15614
15614
  await markFailed(send._id, `provider_unknown: ${send.provider}`, ctx);
15615
15615
  return;
15616
15616
  }
15617
+ const headers = send.kind === "marketing" ? {
15618
+ "List-Unsubscribe": `<${renderCtx.unsubscribeUrl}>`,
15619
+ "List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
15620
+ } : {};
15617
15621
  try {
15618
15622
  const result = await provider.send({
15619
15623
  to: send.emailAtSend,
@@ -15623,10 +15627,7 @@ async function dispatchSend(sendId, ctx) {
15623
15627
  subject: rendered.subject,
15624
15628
  html: tracking.html,
15625
15629
  text: rendered.plainText,
15626
- headers: {
15627
- "List-Unsubscribe": `<${renderCtx.unsubscribeUrl}>`,
15628
- "List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
15629
- },
15630
+ headers,
15630
15631
  messageMeta: { sendId: String(send._id) }
15631
15632
  });
15632
15633
  await ctx.collections.sends.updateOne(
@@ -15738,8 +15739,8 @@ async function processOneRunStep(runId, ctx) {
15738
15739
  return;
15739
15740
  }
15740
15741
  const steps = await loadStepsForRun(run, flow, ctx);
15741
- const step = locateStep(steps, run.currentStepIndex, run.currentBranchPath);
15742
- if (!step) {
15742
+ const step2 = locateStep(steps, run.currentStepIndex, run.currentBranchPath);
15743
+ if (!step2) {
15743
15744
  await completeFlowRun(run, "completed", ctx);
15744
15745
  return;
15745
15746
  }
@@ -15753,34 +15754,34 @@ async function processOneRunStep(runId, ctx) {
15753
15754
  await exitFlowRun(run, sub.status, ctx);
15754
15755
  return;
15755
15756
  }
15756
- switch (step.type) {
15757
+ switch (step2.type) {
15757
15758
  case "wait":
15758
- return handleWait(run, step, ctx);
15759
+ return handleWait(run, step2, ctx);
15759
15760
  case "condition":
15760
- return handleCondition(run, step, contact, ctx);
15761
+ return handleCondition(run, step2, contact, ctx);
15761
15762
  case "branch":
15762
- return handleBranch(run, step, contact, ctx);
15763
+ return handleBranch(run, step2, contact, ctx);
15763
15764
  case "send": {
15764
- if (step.delivery) {
15765
- const deliverAt = computeDeliveryTime(/* @__PURE__ */ new Date(), step.delivery, contact.timezone);
15765
+ if (step2.delivery) {
15766
+ const deliverAt = computeDeliveryTime(/* @__PURE__ */ new Date(), step2.delivery, contact.timezone);
15766
15767
  if (deliverAt.getTime() > Date.now() + 3e4) {
15767
15768
  return deferSendForWindow(run, deliverAt, ctx);
15768
15769
  }
15769
15770
  }
15770
- return handleSend(run, step, contact, flow, ctx);
15771
+ return handleSend(run, step2, contact, flow, ctx);
15771
15772
  }
15772
15773
  case "tag":
15773
- return handleTag(run, step, ctx);
15774
+ return handleTag(run, step2, ctx);
15774
15775
  case "fire_event":
15775
- return handleFireEvent(run, step, ctx);
15776
+ return handleFireEvent(run, step2, ctx);
15776
15777
  case "webhook":
15777
- return handleWebhookStep(run, step, ctx);
15778
+ return handleWebhookStep(run, step2, ctx);
15778
15779
  case "exit":
15779
- return exitFlowRun(run, step.reason ?? "exit_step", ctx);
15780
+ return exitFlowRun(run, step2.reason ?? "exit_step", ctx);
15780
15781
  }
15781
15782
  }
15782
- async function handleWait(run, step, ctx) {
15783
- const ms = unitToMs(step.value, step.unit);
15783
+ async function handleWait(run, step2, ctx) {
15784
+ const ms = unitToMs(step2.value, step2.unit);
15784
15785
  const nextAt = new Date(Date.now() + ms);
15785
15786
  const updated = await ctx.collections.flowRuns.findOneAndUpdate(
15786
15787
  { _id: run._id, currentStepIndex: run.currentStepIndex },
@@ -15805,22 +15806,22 @@ async function handleWait(run, step, ctx) {
15805
15806
  { delay: ms, jobId: `advance:${run._id}:${run.currentStepIndex + 1}` }
15806
15807
  );
15807
15808
  }
15808
- async function handleCondition(run, step, contact, ctx) {
15809
+ async function handleCondition(run, step2, contact, ctx) {
15809
15810
  if (!contact) return;
15810
- const result = await evaluatePredicate(step.test, { contact, run, collections: ctx.collections });
15811
+ const result = await evaluatePredicate(step2.test, { contact, run, collections: ctx.collections });
15811
15812
  if (result) {
15812
15813
  await advanceStep(run, ctx, { action: "condition_evaluated", details: { result: true } });
15813
15814
  return;
15814
15815
  }
15815
- if (step.ifFalse === "continue") {
15816
+ if (step2.ifFalse === "continue") {
15816
15817
  await advanceStep(run, ctx, { action: "condition_evaluated", details: { result: false, skipped: 1 } }, { stepInc: 2 });
15817
15818
  return;
15818
15819
  }
15819
15820
  await exitFlowRun(run, "condition_false", ctx);
15820
15821
  }
15821
- async function handleBranch(run, step, contact, ctx) {
15822
+ async function handleBranch(run, step2, contact, ctx) {
15822
15823
  if (!contact) return;
15823
- const result = await evaluatePredicate(step.test, { contact, run, collections: ctx.collections });
15824
+ const result = await evaluatePredicate(step2.test, { contact, run, collections: ctx.collections });
15824
15825
  const newPath = [...run.currentBranchPath, run.currentStepIndex, result ? "true" : "false", 0];
15825
15826
  const updated = await ctx.collections.flowRuns.findOneAndUpdate(
15826
15827
  { _id: run._id, currentStepIndex: run.currentStepIndex },
@@ -15846,9 +15847,9 @@ async function handleBranch(run, step, contact, ctx) {
15846
15847
  if (!updated) return;
15847
15848
  await ctx.queues.advance.add("advance", { flowRunId: String(run._id) });
15848
15849
  }
15849
- async function handleTag(run, step, ctx) {
15850
- const adds = step.addTags ?? [];
15851
- const removes = step.removeTags ?? [];
15850
+ async function handleTag(run, step2, ctx) {
15851
+ const adds = step2.addTags ?? [];
15852
+ const removes = step2.removeTags ?? [];
15852
15853
  if (ctx.adapter.addTags && adds.length > 0) {
15853
15854
  await ctx.adapter.addTags(run.externalId, adds);
15854
15855
  } else if (adds.length > 0) {
@@ -15869,13 +15870,13 @@ async function handleTag(run, step, ctx) {
15869
15870
  }
15870
15871
  await advanceStep(run, ctx, { action: "tagged", details: { addTags: adds, removeTags: removes } });
15871
15872
  }
15872
- async function handleFireEvent(run, step, ctx) {
15873
- const dedupeKey = `flowrun:${run._id}:step${run.currentStepIndex}:${step.eventName}`;
15873
+ async function handleFireEvent(run, step2, ctx) {
15874
+ const dedupeKey = `flowrun:${run._id}:step${run.currentStepIndex}:${step2.eventName}`;
15874
15875
  try {
15875
15876
  await ctx.collections.events.insertOne({
15876
15877
  externalId: run.externalId,
15877
- name: step.eventName,
15878
- properties: step.properties ?? {},
15878
+ name: step2.eventName,
15879
+ properties: step2.properties ?? {},
15879
15880
  dedupeKey,
15880
15881
  occurredAt: /* @__PURE__ */ new Date(),
15881
15882
  createdAt: /* @__PURE__ */ new Date()
@@ -15883,27 +15884,27 @@ async function handleFireEvent(run, step, ctx) {
15883
15884
  } catch (err) {
15884
15885
  if (err?.code !== 11e3) throw err;
15885
15886
  }
15886
- await advanceStep(run, ctx, { action: "event_fired", details: { name: step.eventName } });
15887
+ await advanceStep(run, ctx, { action: "event_fired", details: { name: step2.eventName } });
15887
15888
  }
15888
- async function handleWebhookStep(run, step, ctx) {
15889
+ async function handleWebhookStep(run, step2, ctx) {
15889
15890
  try {
15890
- const res = await fetch(step.url, {
15891
- method: step.method ?? "POST",
15891
+ const res = await fetch(step2.url, {
15892
+ method: step2.method ?? "POST",
15892
15893
  headers: { "Content-Type": "application/json" },
15893
- body: JSON.stringify(step.payload ?? { externalId: run.externalId, flowRunId: String(run._id) }),
15894
+ body: JSON.stringify(step2.payload ?? { externalId: run.externalId, flowRunId: String(run._id) }),
15894
15895
  signal: AbortSignal.timeout(1e4)
15895
15896
  });
15896
15897
  if (!res.ok) throw new Error(`webhook step returned ${res.status}`);
15897
- await advanceStep(run, ctx, { action: "webhook_called", details: { url: step.url, status: res.status } });
15898
+ await advanceStep(run, ctx, { action: "webhook_called", details: { url: step2.url, status: res.status } });
15898
15899
  } catch (err) {
15899
15900
  const attempts = (run.attemptsForCurrentStep ?? 0) + 1;
15900
15901
  if (attempts >= ctx.config.webhookRetryAttempts) {
15901
- if (step.failureMode === "fail_run") {
15902
+ if (step2.failureMode === "fail_run") {
15902
15903
  await failFlowRun(run, `webhook failed: ${err?.message}`, ctx);
15903
15904
  } else {
15904
15905
  await advanceStep(run, ctx, {
15905
15906
  action: "webhook_called",
15906
- details: { url: step.url, error: err?.message, exhausted: true }
15907
+ details: { url: step2.url, error: err?.message, exhausted: true }
15907
15908
  });
15908
15909
  }
15909
15910
  } else {
@@ -17839,6 +17840,207 @@ function matches(c, f) {
17839
17840
  return true;
17840
17841
  }
17841
17842
 
17843
+ // src/testing/recording-provider.ts
17844
+ var RecordingProvider = class {
17845
+ constructor(inner) {
17846
+ this.inner = inner;
17847
+ }
17848
+ inner;
17849
+ /** Every `SendArgs` handed to the provider, in call order. */
17850
+ sent = [];
17851
+ /** Same calls, with outcome attached. */
17852
+ records = [];
17853
+ get name() {
17854
+ return this.inner.name;
17855
+ }
17856
+ get sendRatePerSecond() {
17857
+ return this.inner.sendRatePerSecond;
17858
+ }
17859
+ async send(args) {
17860
+ this.sent.push(args);
17861
+ const record = { args, result: null, error: null, durationMs: 0, at: /* @__PURE__ */ new Date() };
17862
+ this.records.push(record);
17863
+ const startedAt = Date.now();
17864
+ try {
17865
+ const result = await this.inner.send(args);
17866
+ record.result = result;
17867
+ return result;
17868
+ } catch (err) {
17869
+ record.error = err instanceof Error ? err : new Error(String(err));
17870
+ throw err;
17871
+ } finally {
17872
+ record.durationMs = Date.now() - startedAt;
17873
+ }
17874
+ }
17875
+ async verifyWebhook(rawBody, headers) {
17876
+ return this.inner.verifyWebhook(rawBody, headers);
17877
+ }
17878
+ parseWebhookEvents(payload, headers) {
17879
+ return this.inner.parseWebhookEvents(payload, headers);
17880
+ }
17881
+ /** Last recorded send, or undefined. Sugar for the common single-send assertion. */
17882
+ get last() {
17883
+ return this.sent[this.sent.length - 1];
17884
+ }
17885
+ /** All sends addressed to `email` (case-insensitive). */
17886
+ toRecipient(email) {
17887
+ const lower = email.toLowerCase();
17888
+ return this.sent.filter((s) => s.to.toLowerCase() === lower);
17889
+ }
17890
+ reset() {
17891
+ this.sent.length = 0;
17892
+ this.records.length = 0;
17893
+ }
17894
+ };
17895
+
17896
+ // src/testing/builders.ts
17897
+ var BACKDATE_MS = 6e4;
17898
+ async function buildTemplate(spec) {
17899
+ const now = /* @__PURE__ */ new Date();
17900
+ const createdAt = spec.createdAt ?? new Date(now.getTime() - BACKDATE_MS);
17901
+ let mjml = "";
17902
+ let html;
17903
+ if (spec.html) {
17904
+ html = spec.html;
17905
+ } else {
17906
+ mjml = spec.mjml ?? wrapMjml(spec.text ?? "Hello {{contact.fields.firstName}}");
17907
+ const compiled = await compileTemplate(mjml);
17908
+ html = compiled.html;
17909
+ }
17910
+ const plainText = spec.plainText ?? derivePlaintext(html);
17911
+ return {
17912
+ slug: spec.slug,
17913
+ name: spec.name ?? spec.slug,
17914
+ description: spec.description ?? "",
17915
+ kind: spec.kind ?? "marketing",
17916
+ fromName: spec.fromName ?? "Test",
17917
+ fromEmail: spec.fromEmail ?? "hello@example.com",
17918
+ replyTo: spec.replyTo ?? null,
17919
+ providerOverride: spec.providerOverride ?? null,
17920
+ subject: spec.subject ?? "Test subject",
17921
+ preheader: spec.preheader ?? "",
17922
+ body: { mjml, editorJson: null, html, plainText, compiledAt: createdAt },
17923
+ variablesSchema: spec.variablesSchema ?? {},
17924
+ draft: null,
17925
+ tags: spec.tags ?? [],
17926
+ trackOpens: spec.trackOpens ?? false,
17927
+ trackClicks: spec.trackClicks ?? false,
17928
+ stats: {
17929
+ sent: 0,
17930
+ delivered: 0,
17931
+ opened: 0,
17932
+ clicked: 0,
17933
+ bounced: 0,
17934
+ complained: 0,
17935
+ unsubscribed: 0,
17936
+ lastSentAt: null
17937
+ },
17938
+ publishedAt: spec.published === false ? null : createdAt,
17939
+ publishedBy: spec.published === false ? null : "test",
17940
+ createdAt,
17941
+ updatedAt: createdAt
17942
+ };
17943
+ }
17944
+ function wrapMjml(body) {
17945
+ return `<mjml><mj-body><mj-section><mj-column><mj-text>${body}</mj-text></mj-column></mj-section></mj-body></mjml>`;
17946
+ }
17947
+ function buildFlow(spec) {
17948
+ const now = /* @__PURE__ */ new Date();
17949
+ const createdAt = spec.createdAt ?? new Date(now.getTime() - BACKDATE_MS);
17950
+ const trigger = spec.trigger ?? {
17951
+ type: "event",
17952
+ eventName: spec.eventName ?? `${spec.slug}:trigger`,
17953
+ once: spec.once ?? true
17954
+ };
17955
+ return {
17956
+ slug: spec.slug,
17957
+ name: spec.name ?? spec.slug,
17958
+ description: spec.description ?? "",
17959
+ trigger,
17960
+ enabled: spec.enabled ?? true,
17961
+ steps: spec.steps,
17962
+ version: spec.version ?? 1,
17963
+ draft: null,
17964
+ goal: spec.goal ?? "activation",
17965
+ audience: spec.audience ?? "test",
17966
+ expectedVolumePerWeek: null,
17967
+ stats: { activeRuns: 0, completedRuns: 0, sendsTotal: 0, sendsLast7Days: 0 },
17968
+ lastTriggerScanAt: null,
17969
+ publishedAt: createdAt,
17970
+ publishedBy: "test",
17971
+ createdAt,
17972
+ updatedAt: createdAt
17973
+ };
17974
+ }
17975
+ var step = {
17976
+ send(templateSlug, opts = {}) {
17977
+ return { type: "send", templateSlug, ...opts };
17978
+ },
17979
+ sendAt(templateSlug, delivery) {
17980
+ return { type: "send", templateSlug, delivery };
17981
+ },
17982
+ wait(value, unit = "minutes") {
17983
+ return { type: "wait", value, unit };
17984
+ },
17985
+ tag(addTags, removeTags) {
17986
+ return { type: "tag", addTags, removeTags };
17987
+ },
17988
+ exit(reason) {
17989
+ return { type: "exit", reason };
17990
+ },
17991
+ fireEvent(eventName, properties) {
17992
+ return { type: "fire_event", eventName, properties };
17993
+ }
17994
+ };
17995
+
17996
+ // src/testing/drive.ts
17997
+ async function drain(ctx, opts = {}) {
17998
+ const maxRounds = opts.maxRounds ?? 100;
17999
+ const shouldDispatch = opts.dispatch !== false;
18000
+ const attempted = /* @__PURE__ */ new Set();
18001
+ const errors = [];
18002
+ let dispatched = 0;
18003
+ let rounds = 0;
18004
+ while (rounds < maxRounds) {
18005
+ rounds++;
18006
+ await processNewlyFiredEventTriggers(ctx);
18007
+ await sweepStrandedFlowRuns(ctx);
18008
+ let didWork = false;
18009
+ if (shouldDispatch) {
18010
+ const queued = await ctx.collections.sends.find({ status: "queued" }, { projection: { _id: 1 } }).toArray();
18011
+ for (const row of queued) {
18012
+ const id = String(row._id);
18013
+ if (attempted.has(id)) continue;
18014
+ attempted.add(id);
18015
+ didWork = true;
18016
+ dispatched++;
18017
+ try {
18018
+ await dispatchSend(row._id, ctx);
18019
+ } catch (err) {
18020
+ errors.push(err instanceof Error ? err : new Error(String(err)));
18021
+ if (opts.throwOnSendError) throw err;
18022
+ }
18023
+ }
18024
+ }
18025
+ const due = await ctx.collections.flowRuns.countDocuments({
18026
+ status: "active",
18027
+ nextActionAt: { $lte: /* @__PURE__ */ new Date() }
18028
+ });
18029
+ if (!didWork && due === 0) {
18030
+ return { rounds, dispatched, errors, settled: true };
18031
+ }
18032
+ }
18033
+ return { rounds, dispatched, errors, settled: false };
18034
+ }
18035
+ async function dispatchQueued(ctx) {
18036
+ const queued = await ctx.collections.sends.find({ status: "queued" }, { projection: { _id: 1 } }).toArray();
18037
+ for (const row of queued) {
18038
+ await dispatchSend(row._id, ctx).catch(() => {
18039
+ });
18040
+ }
18041
+ return queued.length;
18042
+ }
18043
+
17842
18044
  // src/testing/index.ts
17843
18045
  async function createTestMailer(opts = {}) {
17844
18046
  const { MongoMemoryServer } = await Promise.resolve().then(() => __toESM(require_mongodb_memory_server()));
@@ -17848,13 +18050,13 @@ async function createTestMailer(opts = {}) {
17848
18050
  const db = client.db("mailery-test");
17849
18051
  const memoryAdapter = opts.adapter ? null : new MemoryContactAdapter(opts.seedContacts ?? []);
17850
18052
  const adapter = opts.adapter ?? memoryAdapter;
17851
- const provider = opts.provider ?? new NullProvider();
18053
+ const provider = new RecordingProvider(await resolveProvider(opts.provider ?? "null"));
17852
18054
  const mailer = await Mailer.init({
17853
18055
  db,
17854
18056
  adapter,
17855
- queue: { driver: "noop" },
17856
- providers: { null: provider },
17857
- defaultProvider: "null",
18057
+ queue: opts.queue ?? { driver: "noop" },
18058
+ providers: { [provider.name]: provider },
18059
+ defaultProvider: provider.name,
17858
18060
  publicUrl: "http://localhost:3000",
17859
18061
  unsubscribeSecret: "test-unsub-secret-32-bytes-or-more-please",
17860
18062
  senderAddress: "1 Test St, Brooklyn NY 11201",
@@ -17862,17 +18064,72 @@ async function createTestMailer(opts = {}) {
17862
18064
  workerless: true,
17863
18065
  ...opts.config ?? {}
17864
18066
  });
17865
- const stop = async () => {
17866
- await mailer.stop().catch(() => {
17867
- });
17868
- await client.close().catch(() => {
17869
- });
17870
- await mongo.stop().catch(() => {
17871
- });
18067
+ if (opts.startWorkers) await mailer.startWorkers();
18068
+ const ctx = mailer.getRunnerContext();
18069
+ const harness = {
18070
+ mailer,
18071
+ db,
18072
+ provider,
18073
+ adapter,
18074
+ memoryAdapter,
18075
+ ctx,
18076
+ async seedContact(contact, seedOpts = {}) {
18077
+ if (!memoryAdapter) {
18078
+ throw new Error("seedContact requires the built-in MemoryContactAdapter \u2014 you passed a custom `adapter`.");
18079
+ }
18080
+ memoryAdapter.upsert(contact);
18081
+ if (seedOpts.subscribe !== false) {
18082
+ await mailer.upsertSubscription({
18083
+ externalId: contact.externalId,
18084
+ source: seedOpts.source ?? "test"
18085
+ });
18086
+ }
18087
+ return contact;
18088
+ },
18089
+ async seedTemplate(spec) {
18090
+ const doc = await buildTemplate(spec);
18091
+ const res = await ctx.collections.templates.insertOne(doc);
18092
+ doc._id = res.insertedId;
18093
+ return doc;
18094
+ },
18095
+ async seedFlow(spec) {
18096
+ const doc = buildFlow(spec);
18097
+ const res = await ctx.collections.flows.insertOne(doc);
18098
+ doc._id = res.insertedId;
18099
+ return doc;
18100
+ },
18101
+ drain(drainOpts) {
18102
+ return drain(ctx, drainOpts);
18103
+ },
18104
+ stop: async () => {
18105
+ await mailer.stop().catch(() => {
18106
+ });
18107
+ await client.close().catch(() => {
18108
+ });
18109
+ await mongo.stop().catch(() => {
18110
+ });
18111
+ }
17872
18112
  };
17873
- return { mailer, db, provider, adapter, memoryAdapter, stop };
18113
+ return harness;
18114
+ }
18115
+ async function resolveProvider(spec) {
18116
+ if (typeof spec !== "string") return spec;
18117
+ if (spec === "null") return new NullProvider();
18118
+ const apiKey = process.env.SENDGRID_API_KEY;
18119
+ if (!apiKey) {
18120
+ throw new Error(
18121
+ "createTestMailer({ provider: 'sendgrid' }) requires SENDGRID_API_KEY. Live tests should gate on it before constructing the harness."
18122
+ );
18123
+ }
18124
+ const { SendGridProvider: SendGridProvider2 } = await Promise.resolve().then(() => (init_sendgrid(), sendgrid_exports));
18125
+ return new SendGridProvider2({
18126
+ apiKey,
18127
+ // Sandbox unless explicitly told to deliver: SendGrid still authenticates
18128
+ // and validates the payload, it just never puts anything in an inbox.
18129
+ sandbox: process.env.MAILERY_LIVE_E2E !== "deliver"
18130
+ });
17874
18131
  }
17875
18132
 
17876
- export { MemoryContactAdapter, NullProvider, createTestMailer };
18133
+ export { MemoryContactAdapter, NullProvider, RecordingProvider, buildFlow, buildTemplate, createTestMailer, dispatchQueued, drain, step, wrapMjml };
17877
18134
  //# sourceMappingURL=testing.js.map
17878
18135
  //# sourceMappingURL=testing.js.map