// Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT package mail import ( "encoding/json" "os" "strings" "testing" ) // ===================================================================== // +lint-html Shortcut tests — public stdout envelope contract checks. // // These exercise the full cobra Mount → Execute pipeline (parse args → // Validate → Execute → OutFormat) so they catch any regression in flag // declaration, mutual-exclusion validation, path safety, and the JSON // envelope shape. // ===================================================================== // TestMailLintHTML_RequiresExactlyOneOfBodyOrFile verifies the mutual- // exclusion + at-least-one-of constraint surfaces ErrValidation. func TestMailLintHTML_RequiresExactlyOneOfBodyOrFile(t *testing.T) { f, stdout, _, _ := mailShortcutTestFactory(t) t.Run("neither flag", func(t *testing.T) { err := runMountedMailShortcut(t, MailLintHTML, []string{"+lint-html"}, f, stdout) if err == nil { t.Fatal("expected error when neither flag is set") } if !strings.Contains(err.Error(), "exactly one of --body or --body-file") { t.Errorf("wrong error: %v", err) } }) t.Run("both flags", func(t *testing.T) { err := runMountedMailShortcut(t, MailLintHTML, []string{ "+lint-html", "--body", "
x
", "--body-file", "fake.html", }, f, stdout) if err == nil { t.Fatal("expected error when both flags set") } if !strings.Contains(err.Error(), "mutually exclusive") { t.Errorf("wrong error: %v", err) } }) } // TestMailLintHTML_BodyFilePathSafetyRejected verifies absolute paths / // `..` traversal are rejected by the path safety check. func TestMailLintHTML_BodyFilePathSafetyRejected(t *testing.T) { f, stdout, _, _ := mailShortcutTestFactory(t) chdirTemp(t) t.Run("absolute path", func(t *testing.T) { err := runMountedMailShortcut(t, MailLintHTML, []string{ "+lint-html", "--body-file", "/etc/passwd", }, f, stdout) if err == nil { t.Fatal("expected validation error for absolute path") } }) t.Run("dotdot traversal", func(t *testing.T) { err := runMountedMailShortcut(t, MailLintHTML, []string{ "+lint-html", "--body-file", "../../../etc/passwd", }, f, stdout) if err == nil { t.Fatal("expected validation error for traversal") } }) } // TestMailLintHTML_BodyFileReadsCwdSubpath verifies a legitimate cwd-subtree // path loads HTML correctly. func TestMailLintHTML_BodyFileReadsCwdSubpath(t *testing.T) { f, stdout, _, _ := mailShortcutTestFactory(t) chdirTemp(t) if err := os.WriteFile("input.html", []byte(`safe
`), 0o644); err != nil { t.Fatal(err) } err := runMountedMailShortcut(t, MailLintHTML, []string{ "+lint-html", "--body-file", "input.html", "--show-lint-details", }, f, stdout) if err != nil { t.Fatalf("expected success, got: %v", err) } data := decodeShortcutEnvelopeData(t, stdout) errors, _ := data["errors"].([]interface{}) if len(errors) != 1 { t.Errorf("expected 1 error finding (script), got %d: %+v", len(errors), errors) } cleaned, _ := data["cleaned_html"].(string) if strings.Contains(cleaned, "`, "--show-lint-details", }, f, stdout) if err != nil { t.Fatalf("unexpected error: %v", err) } data := decodeShortcutEnvelopeData(t, stdout) errors, _ := data["errors"].([]interface{}) if len(errors) == 0 { t.Fatal("expected at least 1 error finding") } first, _ := errors[0].(map[string]interface{}) for _, key := range []string{"rule_id", "severity", "tag_or_attr", "excerpt", "hint"} { if _, ok := first[key]; !ok { t.Errorf("finding missing required key %q: %+v", key, first) } } if first["severity"] != "error" { t.Errorf("severity = %v, want error", first["severity"]) } if !strings.HasPrefix(first["rule_id"].(string), "TAG_") && !strings.HasPrefix(first["rule_id"].(string), "ATTR_") && !strings.HasPrefix(first["rule_id"].(string), "STYLE_") { t.Errorf("rule_id must be UPPER_SNAKE_CASE prefix, got %v", first["rule_id"]) } } // TestMailLintHTML_DryRun verifies dry-run mode doesn't execute lint and // surfaces the read-only / no-network annotation. func TestMailLintHTML_DryRun(t *testing.T) { f, stdout, _, _ := mailShortcutTestFactory(t) err := runMountedMailShortcut(t, MailLintHTML, []string{ "+lint-html", "--body", `x
`, "--dry-run", }, f, stdout) if err != nil { t.Fatalf("unexpected error: %v", err) } // Dry-run output is JSON containing "mode":"local-lint-only". if !strings.Contains(stdout.String(), "local-lint-only") { t.Errorf("expected dry-run mode marker, stdout=%s", stdout.String()) } } // TestMailLintHTML_BlockedTagAndWarningAccumulate verifies the report // surfaces both warning + error findings simultaneously. func TestMailLintHTML_BlockedTagAndWarningAccumulate(t *testing.T) { f, stdout, _, _ := mailShortcutTestFactory(t) body := `warn-tag` + `err-url` err := runMountedMailShortcut(t, MailLintHTML, []string{ "+lint-html", "--body", body, "--show-lint-details", }, f, stdout) if err != nil { t.Fatalf("unexpected error: %v", err) } data := decodeShortcutEnvelopeData(t, stdout) w, _ := data["warnings"].([]interface{}) e, _ := data["errors"].([]interface{}) if len(w) < 1 { t.Errorf("expected ≥ 1 warning, got %d", len(w)) } if len(e) < 2 { t.Errorf("expected ≥ 2 errors (script + js URL), got %d", len(e)) } } // TestMailLintHTML_FindingsAreJSONSerialisable confirms the cleaned envelope // can round-trip through json (no nil / function values leak in). func TestMailLintHTML_FindingsAreJSONSerialisable(t *testing.T) { f, stdout, _, _ := mailShortcutTestFactory(t) err := runMountedMailShortcut(t, MailLintHTML, []string{ "+lint-html", "--body", `x`, }, f, stdout) if err != nil { t.Fatalf("unexpected error: %v", err) } // Re-encode the data back to JSON to confirm it's serialisable. data := decodeShortcutEnvelopeData(t, stdout) if _, err := json.Marshal(data); err != nil { t.Errorf("envelope not JSON-serialisable: %v", err) } }