diff --git a/internal/email/ri_exchange_links_test.go b/internal/email/ri_exchange_links_test.go new file mode 100644 index 000000000..d84eb03df --- /dev/null +++ b/internal/email/ri_exchange_links_test.go @@ -0,0 +1,71 @@ +package email + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Issue #579: the RI-exchange emails must link to the SPA deep link +// (/inventory/ri-exchange/{approve,reject}/:id?token=), which confirms and +// then POSTs. The API paths only route POST, so a click (a GET) returned 401. +// The history links must open the RI Exchange sub-tab; /#ri-exchange landed +// on Home because the SPA routes by path, not by hash. + +const riExchangeLinksDashboard = "https://dashboard.example.com" + +func riExchangeLinksData() RIExchangeNotificationData { + return RIExchangeNotificationData{ + DashboardURL: riExchangeLinksDashboard, + TotalPayment: "125.50", + Exchanges: []RIExchangeItem{{ + RecordID: "rec-579", ApprovalToken: "tok/+&=", + SourceRIID: "ri-0123456789abcdef0", SourceInstanceType: "m5.large", + TargetInstanceType: "m6i.large", TargetCount: 1, + PaymentDue: "125.50", UtilizationPct: 42.5, + }}, + } +} + +const ( + riExchangeApproveLink = riExchangeLinksDashboard + "/inventory/ri-exchange/approve/rec-579?token=tok%2F%2B%26%3D" + riExchangeRejectLink = riExchangeLinksDashboard + "/inventory/ri-exchange/reject/rec-579?token=tok%2F%2B%26%3D" + riExchangeHistoryLink = riExchangeLinksDashboard + "/inventory/ri-exchange" +) + +// assertNoBrokenRIExchangeLinks rejects the two link shapes #579 replaced. +func assertNoBrokenRIExchangeLinks(t *testing.T, body string) { + t.Helper() + assert.NotContains(t, body, "/api/ri-exchange/approve/", "approve link must not hit the POST-only API route") + assert.NotContains(t, body, "/api/ri-exchange/reject/", "reject link must not hit the POST-only API route") + assert.NotContains(t, body, "/#ri-exchange", "history link must use the /inventory/ri-exchange path") +} + +func TestRIExchangePendingApprovalEmail_LinksToSPADeepLink(t *testing.T) { + body, err := RenderRIExchangePendingApprovalEmail(riExchangeLinksData()) + require.NoError(t, err) + + assert.Contains(t, body, "Approve: "+riExchangeApproveLink+"\n") + assert.Contains(t, body, "Reject: "+riExchangeRejectLink+"\n") + assert.Contains(t, body, "\n"+riExchangeHistoryLink+"\n") + assertNoBrokenRIExchangeLinks(t, body) +} + +func TestRIExchangePendingApprovalEmailHTML_LinksToSPADeepLink(t *testing.T) { + html, err := RenderRIExchangePendingApprovalEmailHTML(riExchangeLinksData()) + require.NoError(t, err) + + assert.Contains(t, html, `href="`+riExchangeApproveLink+`"`) + assert.Contains(t, html, `href="`+riExchangeRejectLink+`"`) + assert.Contains(t, html, `href="`+riExchangeHistoryLink+`"`) + assertNoBrokenRIExchangeLinks(t, html) +} + +func TestRIExchangeCompletedEmail_HistoryLinkOpensRIExchangeTab(t *testing.T) { + body, err := RenderRIExchangeCompletedEmail(outcomeUnknownExchangeData()) + require.NoError(t, err) + + assert.Contains(t, body, "View exchange history:\n"+riExchangeHistoryLink+"\n") + assert.NotContains(t, body, "/#ri-exchange") +} diff --git a/internal/email/template_renderers_test.go b/internal/email/template_renderers_test.go index 249aed917..5cc0c7ec4 100644 --- a/internal/email/template_renderers_test.go +++ b/internal/email/template_renderers_test.go @@ -614,8 +614,8 @@ func TestRenderRIExchangePendingApprovalEmail_Issue296(t *testing.T) { // Labeled action URLs (not bracket notation). assert.Contains(t, body, "Approve: ") assert.Contains(t, body, "Reject: ") - assert.Contains(t, body, "/api/ri-exchange/approve/rec-001") - assert.Contains(t, body, "/api/ri-exchange/reject/rec-001") + assert.Contains(t, body, "/inventory/ri-exchange/approve/rec-001") + assert.Contains(t, body, "/inventory/ri-exchange/reject/rec-001") // Custom cancellation-window note overrides the generic fallback. assert.Contains(t, body, "RI exchanges are irreversible once accepted by AWS.") @@ -671,8 +671,8 @@ func TestRenderRIExchangePendingApprovalEmailHTML_Issue296(t *testing.T) { assert.NotEmpty(t, html) // Inline-styled approve anchor with correct href. - assert.Contains(t, html, `href="https://dashboard.example.com/api/ri-exchange/approve/rec-001?token=tok-abc"`) - assert.Contains(t, html, `href="https://dashboard.example.com/api/ri-exchange/reject/rec-001?token=tok-abc"`) + assert.Contains(t, html, `href="https://dashboard.example.com/inventory/ri-exchange/approve/rec-001?token=tok-abc"`) + assert.Contains(t, html, `href="https://dashboard.example.com/inventory/ri-exchange/reject/rec-001?token=tok-abc"`) assert.Contains(t, html, ">Approve<") assert.Contains(t, html, ">Reject<") diff --git a/internal/email/templates.go b/internal/email/templates.go index b7a1be813..dd0133118 100644 --- a/internal/email/templates.go +++ b/internal/email/templates.go @@ -313,8 +313,8 @@ Proposed Exchanges: - Source: {{.SourceRIID}} ({{.SourceInstanceType}}, {{printf "%.1f" .UtilizationPct}}% utilized) Target: {{.TargetInstanceType}} x{{.TargetCount}} Payment Due: ${{.PaymentDue}} - Approve: {{$.DashboardURL}}/api/ri-exchange/approve/{{.RecordID}}?token={{urlquery .ApprovalToken}} - Reject: {{$.DashboardURL}}/api/ri-exchange/reject/{{.RecordID}}?token={{urlquery .ApprovalToken}} + Approve: {{$.DashboardURL}}/inventory/ri-exchange/approve/{{.RecordID}}?token={{urlquery .ApprovalToken}} + Reject: {{$.DashboardURL}}/inventory/ri-exchange/reject/{{.RecordID}}?token={{urlquery .ApprovalToken}} {{end}} Total Payment: ${{.TotalPayment}} {{if .Skipped}} @@ -325,7 +325,7 @@ Skipped (could not process): {{if .CancellationWindowNote}}{{.CancellationWindowNote}}{{else}}Please approve within 6 hours (before the next analysis run).{{end}} Review exchange history: -{{.DashboardURL}}/#ri-exchange +{{.DashboardURL}}/inventory/ri-exchange This is an automated message from CUDly. Do not share these links. ` @@ -371,8 +371,8 @@ const riExchangePendingApprovalHTMLTemplate = `
This is an automated message from CUDly. Do not share these links. View exchange history
+This is an automated message from CUDly. Do not share these links. View exchange history