Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions internal/email/ri_exchange_links_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
8 changes: 4 additions & 4 deletions internal/email/template_renderers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down Expand Up @@ -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<")

Expand Down
14 changes: 7 additions & 7 deletions internal/email/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand All @@ -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.
`
Expand Down Expand Up @@ -371,8 +371,8 @@ const riExchangePendingApprovalHTMLTemplate = `<!DOCTYPE html>
<td align="right" style="padding:10px 12px;color:#475569;">{{printf "%.1f" .UtilizationPct}}%</td>
<td align="right" style="padding:10px 12px;color:#0f172a;font-weight:600;">${{.PaymentDue}}</td>
<td style="padding:10px 12px;">
<a href="{{$.DashboardURL}}/api/ri-exchange/approve/{{.RecordID}}?token={{urlquery .ApprovalToken}}" style="display:inline-block;padding:6px 14px;background:#16a34a;color:#ffffff;text-decoration:none;font-weight:600;font-size:12px;border-radius:4px;margin-right:4px;">Approve</a>
<a href="{{$.DashboardURL}}/api/ri-exchange/reject/{{.RecordID}}?token={{urlquery .ApprovalToken}}" style="display:inline-block;padding:6px 14px;background:#ffffff;color:#dc2626;text-decoration:none;font-weight:600;font-size:12px;border-radius:4px;border:1px solid #dc2626;">Reject</a>
<a href="{{$.DashboardURL}}/inventory/ri-exchange/approve/{{.RecordID}}?token={{urlquery .ApprovalToken}}" style="display:inline-block;padding:6px 14px;background:#16a34a;color:#ffffff;text-decoration:none;font-weight:600;font-size:12px;border-radius:4px;margin-right:4px;">Approve</a>
<a href="{{$.DashboardURL}}/inventory/ri-exchange/reject/{{.RecordID}}?token={{urlquery .ApprovalToken}}" style="display:inline-block;padding:6px 14px;background:#ffffff;color:#dc2626;text-decoration:none;font-weight:600;font-size:12px;border-radius:4px;border:1px solid #dc2626;">Reject</a>
</td>
</tr>
{{end}}</tbody></table>
Expand All @@ -395,7 +395,7 @@ const riExchangePendingApprovalHTMLTemplate = `<!DOCTYPE html>
</td></tr>

<tr><td style="padding:16px 32px;background:#f8fafc;border-top:1px solid #e2e8f0;border-radius:0 0 8px 8px;">
<p style="margin:0;color:#94a3b8;font-size:11px;">This is an automated message from CUDly. Do not share these links. <a href="{{.DashboardURL}}/#ri-exchange" style="color:#64748b;">View exchange history</a></p>
<p style="margin:0;color:#94a3b8;font-size:11px;">This is an automated message from CUDly. Do not share these links. <a href="{{.DashboardURL}}/inventory/ri-exchange" style="color:#64748b;">View exchange history</a></p>
</td></tr>

</table>
Expand Down Expand Up @@ -438,7 +438,7 @@ Skipped (could not process):
- {{.SourceRIID}} ({{.SourceInstanceType}}): {{.Reason}}
{{end}}{{end}}
View exchange history:
{{.DashboardURL}}/#ri-exchange
{{.DashboardURL}}/inventory/ri-exchange

This is an automated message from CUDly.
`
Expand Down
Loading