Skip to content

Send a test email for a mailing list - #503

Merged
pierre-b merged 3 commits into
devfrom
feat/test-email-list
Nov 10, 2026
Merged

pierre-b merged 3 commits into
devfrom
feat/test-email-list

Conversation

@pierre-b

@pierre-b pierre-b commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Closes #423.

What changes

The console's Send Test Email dialog has a new Mailing list field, on the templates and broadcasts pages. A test sent for a list carries that list's real unsubscribe_url, oneclick_unsubscribe_url, confirm_subscription_url and {{ list.name }}, plus the List-Unsubscribe and List-Unsubscribe-Post headers. Unsubscribing from the test works, even for a recipient who is not on the list.

  • Broadcast test: the audience list is preselected. For a notice broadcast (exclude_unsubscribed = false), the test for that list keeps the links but leaves out the header, as the broadcast does.
  • Transactional notifications: the dialog has no list field there, because a real transactional send carries no list links.
  • API: POST /api/transactional.testTemplate takes optional list_id and omit_list_unsubscribe_header.
    • A test for a list needs lists:read on top of transactional:write.
    • An unknown or deleted list answers 404 and creates no contact.
  • History row: the test's message_history row carries the list, as a real send's does. So unsubscribing or confirming from the test, or a bounce or complaint reported on it, changes the recipient's subscription to the list. The dialog, the docs and the CHANGELOG say so.
  • Fix: the test's history row is now recorded under the contact's lowercase address. A test typed with capitals now shows in the contact's history, is erased with the contact, and its unsubscribe link matches.

Version 43.1. No migration and no environment variable. The web analytics SDK bundle is rebuilt for the version.

Plan: plans/feat-test-email-list-plan.md. Docs: Notifuse/docs#26.

Tests

  • Backend unit tests:
    • domain: validation of list_id
    • service: list links, header, history list, unknown list, missing lists:read, no list, caller URL replaced, header omitted, addresses typed with capitals
    • handler: fields passed through, 404
  • Integration (TestListUnsubscribeHeaders/test email for a list), through Mailpit:
    • list links and both headers, plus a working one-click POST for a recipient typed with capitals who is not on the list
    • no header when it is left out, and none without a list
    • a bounce on the test marks the subscriber bounced on the list
    • an unknown list answers 404 and creates no contact
  • Console:
    • modal: default list, notice broadcast, list picked from the field, another list in a notice broadcast, reopening, sending disabled while the preselected list loads, unknown default, cleared list, field hidden without Lists read
    • API client, testEmailListProps, and the 8 catalogues
  • Commands run:
    • go test -race ./internal/... ./pkg/... and the integration test above
    • full npx vitest run, npx tsc -b, npx eslint . (warnings unchanged from dev)
    • SDK build in sync

Known, not handled

  • The foo/bar placeholder list stays when no list is chosen.
  • The broadcast page's test button checks templates:read and contacts:write, while the endpoint requires transactional:write.
  • A test sent from a broadcast does not apply the broadcast's tracking mode, UTM parameters or broadcast variables.
  • email_options.list_unsubscribe_url is still accepted as given from API callers without a list.
  • If the lists fail to load, the dialog shows no list and a broadcast test is sent without one.
  • web_analytics_sdk/package-lock.json still carries 42.3.0, as on dev.

A test email sent from the console's Send Test Email dialog can now be
sent for a mailing list. It carries that list's real unsubscribe and
confirmation links and the List-Unsubscribe headers, and unsubscribing
from it works. A broadcast's test preselects its audience list and, for
a notice broadcast, leaves the header out as the broadcast does.

The test's history row is now recorded under the contact's lowercase
address, so unsubscribing from a test typed with capitals works.

Refs #423
Leave the list field out of the transactional notification test, whose
real sends carry no list. Omit the header only for the broadcast's own
audience list, show a loading state while lists load, and say in the
dialog, the docs and the changelog that a bounce or complaint on a test
for a list changes the subscription. Cover a list picked from the field,
reopening the dialog, the placeholder list, a bounce on the test and an
unknown list through the full stack.
The send button stays disabled while a preselected list loads, so a
quick send from a broadcast no longer goes out without its list. Narrow
the notice broadcast wording to tests sent for its audience list.
@codecov

codecov Bot commented Nov 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.73%. Comparing base (c859e1c) to head (26bd62c).

Additional details and impacted files
@@           Coverage Diff           @@
##              dev     #503   +/-   ##
=======================================
  Coverage   88.73%   88.73%           
=======================================
  Files         365      365           
  Lines       71197    71231   +34     
=======================================
+ Hits        63174    63209   +35     
+ Misses       5397     5396    -1     
  Partials     2626     2626           
Flag Coverage Δ
unit 88.73% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pierre-b
pierre-b merged commit f7699b8 into dev Nov 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant