Test the email
One inquiry reached the inbox. We also need to know what the code does when sending fails or a visitor submits invalid details. We'll run those cases with a fake sender, so the checks won't fill the owner inbox.
In the website folder, check that the integration is saved on our feature branch:
git branch --show-current
git statusStay on workshop-interest. Production email is still disabled.
Outcome
Run four automated tests that check validation and email outcomes without contacting Resend.
Hands-on exercise 4.5
Read what each test checks
An automated test runs code and compares its result with an expected result. Our helper accepts a sender function, so the tests can supply one that reports success or failure without making a network request.
Focus on the test names and their expectations. You should be able to explain a failure in terms of what the form would do. The complete test file is available below; writing TypeScript assertions from scratch can wait.
| Test | What a passing result tells us |
|---|---|
| Invalid input never sends | Invalid email, missing consent, an unknown workshop, and a filled honeypot stop before sending |
| Missing settings never send | The helper returns an error and records the unconfigured event |
| Recipient stays fixed and success needs acceptance | Submitted data can't change the owner recipient; the visitor becomes reply-to |
| Rejections and exceptions produce safe errors | Both failure paths return a useful error without exposing provider details |
These tests check the helper's decisions. We still need a deployed preview and inbox check before release, and we'll inspect retained form input during the browser failure exercise in 5.3.
Add the tests
If the project has no TypeScript test runner, install the course version of tsx as a development dependency:
npm install --save-dev tsx@4.23.13For pnpm, use pnpm add --save-dev tsx@4.23.13. Keep the package file and existing lockfile changes for the commit.
Start fx with /permissions ask and request the checks:
Add tests/course-interest.test.ts using node:test and node:assert/strict.
Test submitInterest from lib/submit-interest.ts with fake send and
log functions. No network requests or real credentials.
Cover invalid input, missing settings, a fixed owner recipient with
the visitor as reply-to, provider rejection, and a thrown send error.
Assert that invalid input and missing configuration never call send.
Verify safe error messages and the fixed event names. Use fictional
values and the existing gardening workshop slug. Keep application
code unchanged. Do not install packages, read .env files, commit,
push, or deploy.Review the created test file. It should import the helper directly and use fake functions for sending. Compare it with the complete reference if fx misses a case or tries to call Resend.
Advanced: complete test file (optional)
import assert from "node:assert/strict";
import { test } from "node:test";
import { submitInterest } from "../lib/submit-interest";
function form() {
const value = new FormData();
value.set("workshopSlug", "a-garden-in-a-pot");
value.set("name", "Alex Maker");
value.set("email", "alex@example.com");
value.set("consent", "on");
return value;
}
const config = {
apiKey: "test-only-not-a-key",
from: "studio@example.com",
to: "owner@example.com",
};
test("invalid input never sends", async () => {
for (const [field, value] of [
["email", "invalid"], ["consent", "off"],
["workshopSlug", "unknown"], ["website", "filled"],
]) {
const input = form();
input.set(field!, value!);
const state = await submitInterest(input, config,
async () => { assert.fail("must not send"); }, () => {});
assert.equal(state.status, "error");
}
});
test("missing settings never send", async () => {
const events: string[] = [];
const state = await submitInterest(form(), {},
async () => { assert.fail("must not send"); },
event => events.push(event));
assert.equal(state.status, "error");
assert.deepEqual(events, ["interest.email_unconfigured"]);
});
test("recipient stays fixed and success needs acceptance", async () => {
const input = form();
input.set("to", "someone-else@example.com");
const state = await submitInterest(input, config, async email => {
assert.equal(email.to, "owner@example.com");
assert.equal(email.replyTo, "alex@example.com");
assert.equal(email.subject, "Workshop interest: A garden in a pot");
return { accepted: true };
}, () => {});
assert.equal(state.status, "success");
});
test("rejections and exceptions produce safe errors", async () => {
for (const throws of [false, true]) {
const events: string[] = [];
const state = await submitInterest(form(), config, async () => {
if (throws) throw new Error("private provider detail");
return { accepted: false };
}, event => events.push(event));
assert.equal(state.status, "error");
assert.ok(!state.message.includes("private provider detail"));
assert.deepEqual(events, [throws
? "interest.email_failed" : "interest.email_rejected"]);
}
});Run and read the result
Exit fx and run the tests from the project root:
npx tsx --test tests/course-interest.test.tsFor pnpm, use pnpm exec tsx --test tests/course-interest.test.ts. The reference file contains four tests. A passing run lists all four as passing with no failures. Record your observed result; passing tests don't establish that Preview has the right account settings.
If a test fails, read its name first. Then compare the expected value with the received value. For example, the recipient check should expect the configured owner even when submitted data tries to supply a different destination.
Try It
Run the tests again. The results should be the same, and there should be no new send in Resend from either run.
Open one test and point to the fake sender it supplies. Find the check that would fail if the helper returned the wrong state or sent invalid data. This is enough to explain what that test protects before continuing.
The helper import cannot be found
Check the file map from 4.4. In a project using src/lib, adjust the test's relative import to reach that helper. Keep the application files in their existing locations. A filename or import mismatch needs fixing before the test can check behavior.
A test fails
Compare the test with the intended behavior in the table, then inspect the helper's diff. Ask fx to explain the failure using that test and propose a focused correction. Review any correction and rerun all four cases. Don't weaken an expected result to make the output pass.
Commit
Stage the reviewed test and dependency changes:
git add tests/course-interest.test.ts package.json package-lock.json
git diff --cached
git commit -m "test(interest): cover validation and email failures"Use pnpm-lock.yaml for a pnpm project. If a test uncovered an application defect, review and stage that correction by its exact path too. Keep the commit local for the final preview.
Done-When
- The tests cover the four cases in the table and pass.
- Fake senders make no network requests and contain no credentials.
- You can explain what one test checks and how a failure would appear to a visitor.
- The reviewed tests and any correction are saved on
workshop-interest.
Solution
The complete reference file above checks the helper with fake senders for each outcome. Run it from the website folder and review any failing expectation before committing.
Keep the passing result with your course notes. Next we'll configure protection against repeated requests before enabling email on the public site.
Was this helpful?