diff --git a/internal/email/archera_signup_url_test.go b/internal/email/archera_signup_url_test.go new file mode 100644 index 000000000..08b542528 --- /dev/null +++ b/internal/email/archera_signup_url_test.go @@ -0,0 +1,134 @@ +package email + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Issue #48: the email templates link to the same Archera signup URL as the +// CLI and the web frontend. The expected value is written out literally here +// on purpose rather than read from common.ArcheraSignupURL: if the shared +// constant is ever pointed at a wrong destination, these tests fail instead +// of following it. +const ( + wantArcheraSignupURL = "https://www.archera.ai/cudly" + legacyArcheraSignupURL = "https://archera.ai/signup?mode=cudly" + + // The two partnership disclosures that must stay visible wherever the + // signup link is shown, in the wording the email templates use. + archeraNonGatingDisclosure = "Optional, set up on Archera's site" + archeraSponsorshipDisclosure = "Archera sponsors CUDly's development" +) + +func archeraEmailData() NotificationData { + return NotificationData{ + DashboardURL: "https://dashboard.example.com", + ApprovalToken: "tkn-48", + ExecutionID: "exec-48", + TotalUpfrontCost: 500.00, + TotalSavings: 50.00, + ArcheraEducationURL: "https://dashboard.example.com/archera-insurance", + Recommendations: []RecommendationSummary{{ + Service: "ec2", ResourceType: "m5.large", Region: "us-east-1", + Count: 1, MonthlySavings: 50.00, + }}, + } +} + +// Every template family that embeds the Archera signup link: purchase +// confirmation (text only) and purchase approval request (text and HTML). +var archeraSignupRenderers = []struct { + name string + render func(NotificationData) (string, error) + section string // heading that opens the Archera block in this template + link string // how the URL must appear in the rendered body +}{ + { + name: "purchase confirmation (text)", + render: RenderPurchaseConfirmationEmail, + section: "ARCHERA INSURANCE", + link: "Sign up: " + wantArcheraSignupURL + "\n", + }, + { + name: "purchase approval request (text)", + render: RenderPurchaseApprovalRequestEmail, + section: "ARCHERA INSURANCE", + link: "Sign up: " + wantArcheraSignupURL + "\n", + }, + { + name: "purchase approval request (HTML)", + render: RenderPurchaseApprovalRequestEmailHTML, + section: ">Archera Insurance", + link: `href="` + wantArcheraSignupURL + `"`, + }, +} + +func TestArcheraSignupURL_EmailTemplatesUseCanonicalURL(t *testing.T) { + for _, tc := range archeraSignupRenderers { + t.Run(tc.name, func(t *testing.T) { + body, err := tc.render(archeraEmailData()) + require.NoError(t, err) + + assert.Contains(t, body, tc.link) + assert.NotContains(t, body, legacyArcheraSignupURL) + assert.NotContains(t, body, "archera.ai/signup") + }) + } +} + +// The signup link must never appear without both disclosures in the same +// Archera block. +func TestArcheraSignupURL_DisclosuresStayNextToLink(t *testing.T) { + for _, tc := range archeraSignupRenderers { + t.Run(tc.name, func(t *testing.T) { + body, err := tc.render(archeraEmailData()) + require.NoError(t, err) + + start := strings.Index(body, tc.section) + require.GreaterOrEqual(t, start, 0, "Archera block heading %q not found", tc.section) + block := body[start:] + + assert.Contains(t, block, wantArcheraSignupURL) + assert.Contains(t, block, archeraNonGatingDisclosure) + assert.Contains(t, block, archeraSponsorshipDisclosure) + }) + } +} + +// Without an education URL the whole Archera block, signup link included, is +// left out. +func TestArcheraSignupURL_OmittedWithoutEducationURL(t *testing.T) { + for _, tc := range archeraSignupRenderers { + t.Run(tc.name, func(t *testing.T) { + data := archeraEmailData() + data.ArcheraEducationURL = "" + body, err := tc.render(data) + require.NoError(t, err) + + assert.NotContains(t, body, wantArcheraSignupURL) + }) + } +} + +// No email template source in this package may still carry the old link, +// including templates added later that the renderers above do not cover. +func TestArcheraSignupURL_NoTemplateSourceHasLegacyURL(t *testing.T) { + files, err := filepath.Glob("*.go") + require.NoError(t, err) + require.NotEmpty(t, files) + + for _, f := range files { + if strings.HasSuffix(f, "_test.go") { + continue + } + src, err := os.ReadFile(f) //nolint:gosec // fixed glob over this package's own source files + require.NoError(t, err) + assert.NotContains(t, string(src), legacyArcheraSignupURL, "%s still contains the legacy Archera signup URL", f) + assert.NotContains(t, string(src), "archera.ai/signup", "%s still contains an archera.ai/signup link", f) + } +} diff --git a/internal/email/template_renderers_test.go b/internal/email/template_renderers_test.go index 08422527a..249aed917 100644 --- a/internal/email/template_renderers_test.go +++ b/internal/email/template_renderers_test.go @@ -435,7 +435,7 @@ func TestRenderPurchaseConfirmationEmail_ArcheraBlock(t *testing.T) { assert.Contains(t, body, "Archera") assert.Contains(t, body, "7-day") // confirmation template uses "7-day enrollment window" - assert.Contains(t, body, "https://archera.ai/signup?mode=cudly") + assert.Contains(t, body, "https://www.archera.ai/cudly") assert.Contains(t, body, "https://dashboard.example.com/archera-insurance") } diff --git a/internal/email/templates.go b/internal/email/templates.go index 2b6cb2113..f61f6ebdf 100644 --- a/internal/email/templates.go +++ b/internal/email/templates.go @@ -10,6 +10,11 @@ import ( ) // Email templates +// +// The Archera signup link in the purchase confirmation and approval request +// templates is spliced in from common.ArcheraSignupURL, the constant the CLI +// also uses, so every surface sends buyers to the same attribution URL. The +// frontend keeps its own copy in frontend/src/archera.ts (ARCHERA_SIGNUP_URL). const newRecommendationsTemplate = `CUDly - New Commitment Recommendations Available ================================================ @@ -119,7 +124,7 @@ terms, coverage, and pricing are set entirely by Archera. CUDly has no visibility into your Archera account or policy. Review Archera's terms of service and privacy policy before signing up. -Sign up: https://archera.ai/signup?mode=cudly +Sign up: ` + common.ArcheraSignupURL + ` Permalink: {{.ArcheraEducationURL}} {{end}} This is an automated message from CUDly. @@ -660,7 +665,7 @@ useful, but you should know about the financial relationship. Insurance terms, coverage, and pricing are set entirely by Archera. CUDly has no visibility into the buyer's Archera account or policy. -Sign up: https://archera.ai/signup?mode=cudly +Sign up: ` + common.ArcheraSignupURL + ` Permalink: {{.ArcheraEducationURL}} {{end}} This is an automated message from CUDly. Do not share these links. @@ -758,7 +763,7 @@ const purchaseApprovalRequestHTMLTemplate = `

Archera charges an insurance premium for the coverage selected, a separate fee paid to Archera. The cloud commitment purchased through CUDly is unaffected: same price, same billing.

-

Sign up at Archera →

+

Sign up at Archera →

Full disclosure: Archera sponsors CUDly's development with a share of their insurance revenue; we surface the option because we think it's useful, but you should know about the financial relationship. Insurance terms, coverage, and pricing are set entirely by Archera. CUDly has no visibility into the buyer's Archera account or policy.