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
80 changes: 80 additions & 0 deletions cmd/email_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,84 @@ var emailMessagesPreviewCmd = &cobra.Command{
},
}

func runEmailMessageGuardian(cfg *config.Config, id string) (*loops.EmailMessageGuardianResponse, error) {
return newAPIClient(cfg).GetEmailMessageGuardian(id)
}

var emailMessagesGuardianCmd = &cobra.Command{
Use: "guardian <id>",
Short: "Run Guardian content validation on an email message",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}

resp, err := runEmailMessageGuardian(cfg, args[0])
if err != nil {
return err
}

if isJSONOutput() {
return printJSON(cmd.OutOrStdout(), resp)
}

return printGuardianResponse(cmd, resp)
},
}

func printGuardianResponse(cmd *cobra.Command, resp *loops.EmailMessageGuardianResponse) error {
out := cmd.OutOrStdout()
if len(resp.Errors) == 0 && len(resp.Warnings) == 0 {
fmt.Fprintln(out, "No guardian issues found.")
return nil
}

if err := printGuardianSection(cmd, "Errors", resp.Errors); err != nil {
return err
}
if len(resp.Errors) > 0 && len(resp.Warnings) > 0 {
fmt.Fprintln(out)
}
return printGuardianSection(cmd, "Warnings", resp.Warnings)
}

func printGuardianSection(cmd *cobra.Command, heading string, rules []loops.GuardianRule) error {
if len(rules) == 0 {
return nil
}
fmt.Fprintf(cmd.OutOrStdout(), "%s:\n", heading)
t := newStyledTable(cmd.OutOrStdout(), "RULE", "TITLE", "DESCRIPTION", "ITEMS")
for _, r := range rules {
t.Row(r.Rule, r.Title, r.Description, formatGuardianItems(r.Items))
}
return t.Render()
}

// formatGuardianItems summarizes a rule's items as a count plus a short,
// comma-separated list of item labels (falling back to codeName when a label
// is empty). Returns "0" when there are no items.
func formatGuardianItems(items []loops.GuardianRuleItem) string {
if len(items) == 0 {
return "0"
}
labels := make([]string, 0, len(items))
for _, item := range items {
label := item.Label
if label == "" {
label = item.CodeName
}
if label != "" {
labels = append(labels, label)
}
}
if len(labels) == 0 {
return fmt.Sprintf("%d", len(items))
}
return fmt.Sprintf("%d (%s)", len(items), strings.Join(labels, ", "))
}

func printLmxWarnings(cmd *cobra.Command, warnings []loops.LmxWarning) {
if len(warnings) == 0 {
return
Expand All @@ -460,6 +538,8 @@ func printLmxWarnings(cmd *cobra.Command, warnings []loops.LmxWarning) {
func init() {
emailMessagesCmd.AddCommand(emailMessagesGetCmd)

emailMessagesCmd.AddCommand(emailMessagesGuardianCmd)

addEmailMessageFieldFlags(emailMessagesUpdateCmd)
emailMessagesUpdateCmd.Flags().StringP("expected-revision-id", "r", "", "Last-seen contentRevisionId. Get this from a prior 'email-messages get'. Mutually exclusive with --force.")
emailMessagesUpdateCmd.Flags().BoolP("force", "f", false, "Fetch the current revision and use it (overwrites any concurrent edits). Mutually exclusive with --expected-revision-id.")
Expand Down
87 changes: 87 additions & 0 deletions cmd/email_messages_guardian_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package cmd

import (
"net/http"
"testing"

loops "github.com/loops-so/loops-go"
)

func TestRunEmailMessageGuardian(t *testing.T) {
body := `{
"errors": [
{
"rule": "missing-unsubscribe",
"title": "Missing unsubscribe link",
"description": "Marketing emails must include an unsubscribe link.",
"items": [
{"label": "Footer", "codeName": "footer"}
]
}
],
"warnings": [
{
"rule": "broken-link",
"title": "Broken link",
"description": "One or more links may be broken.",
"items": [
{"label": "https://example.com/a", "codeName": "link-a"},
{"label": "https://example.com/b", "codeName": "link-b"}
]
}
]
}`

t.Run("returns errors and warnings", func(t *testing.T) {
serveJSON(t, http.StatusOK, body)
resp, err := runEmailMessageGuardian(cfg(t), "em_abc123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(resp.Errors) != 1 {
t.Fatalf("Errors len = %d, want 1", len(resp.Errors))
}
if resp.Errors[0].Rule != "missing-unsubscribe" {
t.Errorf("Errors[0].Rule = %q, want missing-unsubscribe", resp.Errors[0].Rule)
}
if len(resp.Errors[0].Items) != 1 || resp.Errors[0].Items[0].Label != "Footer" {
t.Errorf("Errors[0].Items = %+v, want one item labeled Footer", resp.Errors[0].Items)
}
if len(resp.Warnings) != 1 {
t.Fatalf("Warnings len = %d, want 1", len(resp.Warnings))
}
if len(resp.Warnings[0].Items) != 2 {
t.Errorf("Warnings[0].Items len = %d, want 2", len(resp.Warnings[0].Items))
}
})

t.Run("returns error on non-200 response", func(t *testing.T) {
serveJSON(t, http.StatusNotFound, `{"success":false,"message":"Email message not found"}`)
_, err := runEmailMessageGuardian(cfg(t), "em_missing")
if err == nil {
t.Fatal("expected error, got nil")
}
})
}

func TestFormatGuardianItems(t *testing.T) {
tests := []struct {
name string
items []loops.GuardianRuleItem
want string
}{
{"no items", nil, "0"},
{"label", []loops.GuardianRuleItem{{Label: "Footer"}}, "1 (Footer)"},
{"codeName fallback", []loops.GuardianRuleItem{{CodeName: "footer"}}, "1 (footer)"},
{"empty label and codeName", []loops.GuardianRuleItem{{}}, "1"},
{"multiple", []loops.GuardianRuleItem{{Label: "A"}, {Label: "B"}}, "2 (A, B)"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := formatGuardianItems(tc.items)
if got != tc.want {
t.Errorf("formatGuardianItems() = %q, want %q", got, tc.want)
}
})
}
}