Send the invitation as a light letter that names the community, the inviter by email, and who can read the Rule.

This commit is contained in:
adilallo committed 2026-11-06 09:00:44 -07:00
1 parent 70ceb73104
commit 727a92a9ac
14 files changed
+534 -26

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import {
clipCommunityInviteDescription,
communityInviteInitials,
communityPossessive,
inviteRuleCountsLabel,
} from "../../lib/create/ruleInviteCardMeta";
describe("communityInviteInitials", () => {
it("uses the first two words", () => {
expect(communityInviteInitials("Juniper Street Food Share")).toBe("JS");
});
it("uses two letters of a single word", () => {
expect(communityInviteInitials("Garden")).toBe("GA");
});
});
describe("communityPossessive", () => {
it("adds a curly ’s when the name does not end in s", () => {
expect(communityPossessive("Juniper Street Food Share")).toBe(
"Juniper Street Food Share’s",
);
});
it("adds only a curly apostrophe when the name ends in s", () => {
expect(communityPossessive("Members")).toBe("Members’");
});
});
describe("clipCommunityInviteDescription", () => {
it("cuts at 140 characters", () => {
const long = "a".repeat(160);
expect(clipCommunityInviteDescription(long)).toHaveLength(140);
});
});
describe("inviteRuleCountsLabel", () => {
it("counts values and other cards, not shared terms", () => {
expect(
inviteRuleCountsLabel({
sections: [
{
categoryName: "Values",
entries: [
{ title: "Care", body: "We care." },
{ title: "Share", body: "We share." },
{ title: "Listen", body: "We listen." },
],
},
{
categoryName: "Communication",
entries: [{ title: "Open channel", body: "We talk in the hall." }],
},
{
categoryName: "Membership",
entries: [{ title: "Open admission", body: "Anyone who shares the work." }],
},
],
}),
).toBe("3 VALUES AND 2 CARDS");
});
});
@@ -0,0 +1,90 @@
import { describe, expect, it } from "vitest";
import {
RULE_STAKEHOLDER_INVITE_LOGO_PATH,
buildRuleStakeholderInviteMail,
ruleStakeholderInviteCopyUrl,
} from "../../lib/server/ruleStakeholderInviteMail";
const VERIFY_URL =
"https://communityrule.info/api/invites/rule-stakeholder/verify?token=abc";
describe("ruleStakeholderInviteCopyUrl", () => {
it("adds copy=1 to the invitation link", () => {
expect(ruleStakeholderInviteCopyUrl(VERIFY_URL)).toBe(
`${VERIFY_URL}&copy=1`,
);
});
});
describe("buildRuleStakeholderInviteMail", () => {
const mail = buildRuleStakeholderInviteMail({
to: "rosa@example.org",
verifyUrl: VERIFY_URL,
origin: "https://communityrule.info",
communityName: "Juniper Street Food Share",
inviterEmail: "maya@example.org",
summary:
"A food share that runs every Sunday on Juniper Street. Volunteers collect surplus from two grocers and a farm stand and share it with neighbors.",
document: {
sections: [
{
categoryName: "Values",
entries: [
{ title: "Care", body: "We care." },
{ title: "Share", body: "We share." },
{ title: "Listen", body: "We listen." },
],
},
{
categoryName: "Communication",
entries: Array.from({ length: 6 }, (_, i) => ({
title: `Card ${i + 1}`,
body: "Body",
})),
},
],
},
});
it("uses the launch subject, preview, and privacy line", () => {
expect(mail.subject).toBe(
"Juniper Street Food Share invited you to read their Rule",
);
expect(mail.previewText).toBe(
"It covers how they talk, who belongs, how they decide and what happens when they disagree.",
);
expect(mail.text).toContain(
"Every Rule starts private. Only the people Juniper Street Food Share invites can read it until the person who holds the pen turns on a link. A Rule is public and featured only when a community asks us in writing.",
);
expect(mail.html).toContain("Every Rule starts private.");
});
it("names the inviter by email and points replies at that address in copy", () => {
expect(mail.text).toContain("maya@example.org sent the invitation.");
expect(mail.text).toContain(
"how they talk, who belongs, how they decide and what happens when they disagree.",
);
expect(mail.html).not.toContain("Maya Ortiz");
});
it("includes read and copy links, footer, and the 2x mark", () => {
expect(mail.text).toContain(VERIFY_URL);
expect(mail.text).toContain(`${VERIFY_URL}&copy=1`);
expect(mail.html).toContain(`href="${VERIFY_URL}"`);
expect(mail.html).toContain(`href="${VERIFY_URL}&copy=1"`);
expect(mail.html).toContain("Read the Rule");
expect(mail.html).toContain("Make a copy");
expect(mail.html).toContain("3 VALUES AND 6 CARDS");
expect(mail.html).toContain("JS");
expect(mail.html).toContain("#e4e4e4");
expect(mail.html).toContain("font-size:14px;line-height:20px;color:#464646");
expect(mail.html).toContain(
`https://communityrule.info${RULE_STAKEHOLDER_INVITE_LOGO_PATH}`,
);
expect(mail.html).toContain("https://communityrule.info/privacy");
expect(mail.html).toContain("https://communityrule.info/about");
expect(mail.text).toContain(
"Juniper Street Food Share’s Rule on CommunityRule",
);
});
});
+8 -3
View File
@@ -148,9 +148,14 @@ describe("POST /api/rules", () => {
expect(res.status).toBe(200);
expect(transactionMock).toHaveBeenCalledTimes(1);
expect(sendInviteMock).toHaveBeenCalledTimes(1);
expect(sendInviteMock.mock.calls[0][0]).toBe("stakeholder@example.com");
expect(String(sendInviteMock.mock.calls[0][1])).toContain(
"/api/invites/rule-stakeholder/verify?token=",
expect(sendInviteMock).toHaveBeenCalledWith(
expect.objectContaining({
to: "stakeholder@example.com",
communityName: "Published title",
verifyUrl: expect.stringContaining(
"/api/invites/rule-stakeholder/verify?token=",
),
}),
);
expect(publishedRuleCreateMock).not.toHaveBeenCalled();
});
@@ -130,7 +130,13 @@ describe("POST /api/rules/[id]/stakeholders", () => {
routeCtx,
);
expect(res.status).toBe(201);
expect(createInviteMock).toHaveBeenCalled();
expect(createInviteMock).toHaveBeenCalledWith(
expect.objectContaining({
email: "inv@example.com",
ruleTitle: "My rule",
inviterEmail: owner.email,
}),
);
const body = (await res.json()) as {
stakeholder: { email: string; status: string };
};
@@ -214,9 +220,14 @@ describe("POST /api/rules/[id]/stakeholders/[stakeholderId]/resend", () => {
expect(await res.json()).toEqual({ ok: true });
expect(updateMock).toHaveBeenCalled();
expect(sendInviteEmailMock).toHaveBeenCalledWith(
"inv@example.com",
expect.stringContaining("/api/invites/rule-stakeholder/verify?token="),
"My rule",
expect.objectContaining({
to: "inv@example.com",
communityName: "My rule",
inviterEmail: owner.email,
verifyUrl: expect.stringContaining(
"/api/invites/rule-stakeholder/verify?token=",
),
}),
);
});
});