fix(fn-run): propagar stdout/stderr de bash functions library-style #1
@@ -0,0 +1,125 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEmailBuildHTML(t *testing.T) {
|
||||
t.Run("construye mensaje html con campos basicos", func(t *testing.T) {
|
||||
msg := EmailBuildHTML("alice@example.com", []string{"bob@example.com"}, "Hola", "<b>Hola</b>")
|
||||
if msg.From != "alice@example.com" {
|
||||
t.Errorf("From: got %q, want %q", msg.From, "alice@example.com")
|
||||
}
|
||||
if len(msg.To) != 1 || msg.To[0] != "bob@example.com" {
|
||||
t.Errorf("To: got %v, want [bob@example.com]", msg.To)
|
||||
}
|
||||
if msg.Subject != "Hola" {
|
||||
t.Errorf("Subject: got %q, want %q", msg.Subject, "Hola")
|
||||
}
|
||||
if msg.BodyHTML != "<b>Hola</b>" {
|
||||
t.Errorf("BodyHTML: got %q, want %q", msg.BodyHTML, "<b>Hola</b>")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("copia el slice to para evitar aliasing", func(t *testing.T) {
|
||||
to := []string{"a@example.com", "b@example.com"}
|
||||
msg := EmailBuildHTML("x@x.com", to, "s", "h")
|
||||
to[0] = "mutated@example.com"
|
||||
if msg.To[0] == "mutated@example.com" {
|
||||
t.Error("To slice was aliased — mutation affected the message")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("campos no usados quedan vacios", func(t *testing.T) {
|
||||
msg := EmailBuildHTML("f@example.com", []string{"t@example.com"}, "s", "h")
|
||||
if msg.BodyText != "" {
|
||||
t.Errorf("BodyText should be empty, got %q", msg.BodyText)
|
||||
}
|
||||
if len(msg.CC) != 0 {
|
||||
t.Errorf("CC should be empty, got %v", msg.CC)
|
||||
}
|
||||
if len(msg.BCC) != 0 {
|
||||
t.Errorf("BCC should be empty, got %v", msg.BCC)
|
||||
}
|
||||
if len(msg.Attachments) != 0 {
|
||||
t.Errorf("Attachments should be empty, got %v", msg.Attachments)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEmailBuildText(t *testing.T) {
|
||||
t.Run("construye mensaje text con campos basicos", func(t *testing.T) {
|
||||
msg := EmailBuildText("alice@example.com", []string{"bob@example.com"}, "Alerta", "Servidor caido")
|
||||
if msg.BodyText != "Servidor caido" {
|
||||
t.Errorf("BodyText: got %q, want %q", msg.BodyText, "Servidor caido")
|
||||
}
|
||||
if msg.From != "alice@example.com" {
|
||||
t.Errorf("From: got %q, want %q", msg.From, "alice@example.com")
|
||||
}
|
||||
if msg.Subject != "Alerta" {
|
||||
t.Errorf("Subject: got %q, want %q", msg.Subject, "Alerta")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("body html queda vacio", func(t *testing.T) {
|
||||
msg := EmailBuildText("f@x.com", []string{"t@x.com"}, "s", "body")
|
||||
if msg.BodyHTML != "" {
|
||||
t.Errorf("BodyHTML should be empty, got %q", msg.BodyHTML)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEmailWithAttachment(t *testing.T) {
|
||||
t.Run("agrega adjunto sin mutar el original", func(t *testing.T) {
|
||||
orig := EmailBuildHTML("a@example.com", []string{"b@example.com"}, "s", "h")
|
||||
att := EmailAttachment{Filename: "f.pdf", ContentType: "application/pdf", Data: []byte("data")}
|
||||
msg2 := EmailWithAttachment(orig, att)
|
||||
|
||||
if len(orig.Attachments) != 0 {
|
||||
t.Errorf("original was mutated: got %d attachments, want 0", len(orig.Attachments))
|
||||
}
|
||||
if len(msg2.Attachments) != 1 {
|
||||
t.Errorf("got %d attachments, want 1", len(msg2.Attachments))
|
||||
}
|
||||
if msg2.Attachments[0].Filename != "f.pdf" {
|
||||
t.Errorf("Filename: got %q, want %q", msg2.Attachments[0].Filename, "f.pdf")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiples adjuntos se acumulan", func(t *testing.T) {
|
||||
msg := EmailBuildHTML("a@x.com", []string{"b@x.com"}, "s", "h")
|
||||
msg = EmailWithAttachment(msg, EmailAttachment{Filename: "a1.txt", ContentType: "text/plain", Data: []byte("1")})
|
||||
msg = EmailWithAttachment(msg, EmailAttachment{Filename: "a2.txt", ContentType: "text/plain", Data: []byte("2")})
|
||||
if len(msg.Attachments) != 2 {
|
||||
t.Errorf("got %d attachments, want 2", len(msg.Attachments))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("copia todos los campos del mensaje", func(t *testing.T) {
|
||||
orig := EmailMessage{
|
||||
From: "a@x.com",
|
||||
To: []string{"b@x.com"},
|
||||
CC: []string{"c@x.com"},
|
||||
BCC: []string{"d@x.com"},
|
||||
Subject: "Test",
|
||||
BodyHTML: "<p>Hi</p>",
|
||||
BodyText: "Hi",
|
||||
Headers: map[string]string{"X-Test": "value"},
|
||||
}
|
||||
att := EmailAttachment{Filename: "f.png", ContentType: "image/png", Data: []byte("img")}
|
||||
msg2 := EmailWithAttachment(orig, att)
|
||||
|
||||
if msg2.From != orig.From {
|
||||
t.Errorf("From mismatch: %q vs %q", msg2.From, orig.From)
|
||||
}
|
||||
if msg2.Subject != orig.Subject {
|
||||
t.Errorf("Subject mismatch: %q vs %q", msg2.Subject, orig.Subject)
|
||||
}
|
||||
if len(msg2.CC) != 1 || msg2.CC[0] != "c@x.com" {
|
||||
t.Errorf("CC not copied: %v", msg2.CC)
|
||||
}
|
||||
if msg2.Headers["X-Test"] != "value" {
|
||||
t.Errorf("Headers not copied: %v", msg2.Headers)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEmailTemplateRender(t *testing.T) {
|
||||
t.Run("renderiza template simple con datos", func(t *testing.T) {
|
||||
got, err := EmailTemplateRender("Hola {{.Name}}", map[string]any{"Name": "Alice"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != "Hola Alice" {
|
||||
t.Errorf("got %q, want %q", got, "Hola Alice")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sustituye multiples variables", func(t *testing.T) {
|
||||
tmpl := "Pedido {{.OrderID}} para {{.Customer}} listo."
|
||||
got, err := EmailTemplateRender(tmpl, map[string]any{"OrderID": "ORD-42", "Customer": "Bob"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(got, "ORD-42") || !strings.Contains(got, "Bob") {
|
||||
t.Errorf("got %q, expected OrderID and Customer", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error en template invalido", func(t *testing.T) {
|
||||
_, err := EmailTemplateRender("{{.Unclosed", nil)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid template, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("template vacio retorna string vacio", func(t *testing.T) {
|
||||
got, err := EmailTemplateRender("", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != "" {
|
||||
t.Errorf("got %q, want empty string", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mockSMTPServer starts a minimal SMTP server on a random port that greets
|
||||
// and accepts any commands. Returns the listener address and a function to stop it.
|
||||
func mockSMTPServer(t *testing.T) (addr string, stop func()) {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("mock smtp listen: %v", err)
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go handleMockSMTP(conn)
|
||||
}
|
||||
}()
|
||||
return ln.Addr().String(), func() { ln.Close() }
|
||||
}
|
||||
|
||||
// handleMockSMTP is a minimal SMTP session handler (no TLS, no auth required).
|
||||
func handleMockSMTP(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
w := bufio.NewWriter(conn)
|
||||
r := bufio.NewReader(conn)
|
||||
fmt.Fprintf(w, "220 mock SMTP ready\r\n")
|
||||
w.Flush()
|
||||
for {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
upper := strings.ToUpper(line)
|
||||
switch {
|
||||
case strings.HasPrefix(upper, "EHLO"), strings.HasPrefix(upper, "HELO"):
|
||||
fmt.Fprintf(w, "250 mock\r\n")
|
||||
case strings.HasPrefix(upper, "MAIL FROM"):
|
||||
fmt.Fprintf(w, "250 OK\r\n")
|
||||
case strings.HasPrefix(upper, "RCPT TO"):
|
||||
fmt.Fprintf(w, "250 OK\r\n")
|
||||
case strings.HasPrefix(upper, "DATA"):
|
||||
fmt.Fprintf(w, "354 End with .\r\n")
|
||||
w.Flush()
|
||||
// read until ".\r\n"
|
||||
for {
|
||||
dl, derr := r.ReadString('\n')
|
||||
if derr != nil {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(dl) == "." {
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "250 OK\r\n")
|
||||
case strings.HasPrefix(upper, "QUIT"):
|
||||
fmt.Fprintf(w, "221 Bye\r\n")
|
||||
w.Flush()
|
||||
return
|
||||
default:
|
||||
fmt.Fprintf(w, "502 Command not recognized\r\n")
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func TestSMTPConnect(t *testing.T) {
|
||||
t.Run("conecta sin cifrado a servidor mock", func(t *testing.T) {
|
||||
addr, stop := mockSMTPServer(t)
|
||||
defer stop()
|
||||
|
||||
host, portStr, _ := net.SplitHostPort(addr)
|
||||
port := 0
|
||||
fmt.Sscanf(portStr, "%d", &port)
|
||||
|
||||
cfg := SMTPConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
TLSMode: "", // no encryption
|
||||
}
|
||||
client, err := SMTPConnect(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("SMTPConnect: %v", err)
|
||||
}
|
||||
client.Quit()
|
||||
})
|
||||
|
||||
t.Run("error si el servidor no existe", func(t *testing.T) {
|
||||
// Use a port that is listening but immediately closes (RST) — we start
|
||||
// and immediately close a listener so the port is known-closed.
|
||||
ln2, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
h2, p2str, _ := net.SplitHostPort(ln2.Addr().String())
|
||||
ln2.Close() // now nothing listens — connections get refused
|
||||
p2 := 0
|
||||
fmt.Sscanf(p2str, "%d", &p2)
|
||||
|
||||
cfg := SMTPConfig{
|
||||
Host: h2,
|
||||
Port: p2,
|
||||
TLSMode: "",
|
||||
}
|
||||
_, connErr := SMTPConnect(cfg)
|
||||
if connErr == nil {
|
||||
t.Error("expected error for refused connection, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// startMockSMTPForSend starts a mock SMTP server and returns host/port separately.
|
||||
func startMockSMTPForSend(t *testing.T) (host string, port int, stop func()) {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("mock smtp listen: %v", err)
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go handleMockSMTP(conn)
|
||||
}
|
||||
}()
|
||||
h, ps, _ := net.SplitHostPort(ln.Addr().String())
|
||||
p := 0
|
||||
fmt.Sscanf(ps, "%d", &p)
|
||||
return h, p, func() { ln.Close() }
|
||||
}
|
||||
|
||||
|
||||
func TestSMTPSend(t *testing.T) {
|
||||
t.Run("envia mensaje texto plano via mock smtp", func(t *testing.T) {
|
||||
host, port, stop := startMockSMTPForSend(t)
|
||||
defer stop()
|
||||
|
||||
cfg := SMTPConfig{Host: host, Port: port, TLSMode: ""}
|
||||
client, err := SMTPConnect(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("SMTPConnect: %v", err)
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
msg := EmailBuildText("alice@example.com", []string{"bob@example.com"}, "Test", "Hello Bob")
|
||||
if err := SMTPSend(client, msg); err != nil {
|
||||
t.Fatalf("SMTPSend: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("envia mensaje html via mock smtp", func(t *testing.T) {
|
||||
host, port, stop := startMockSMTPForSend(t)
|
||||
defer stop()
|
||||
|
||||
cfg := SMTPConfig{Host: host, Port: port, TLSMode: ""}
|
||||
client, err := SMTPConnect(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("SMTPConnect: %v", err)
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
msg := EmailBuildHTML("alice@example.com", []string{"bob@example.com"}, "HTML Test", "<b>Hello</b>")
|
||||
if err := SMTPSend(client, msg); err != nil {
|
||||
t.Fatalf("SMTPSend: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("envia con adjunto via mock smtp", func(t *testing.T) {
|
||||
host, port, stop := startMockSMTPForSend(t)
|
||||
defer stop()
|
||||
|
||||
cfg := SMTPConfig{Host: host, Port: port, TLSMode: ""}
|
||||
client, err := SMTPConnect(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("SMTPConnect: %v", err)
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
msg := EmailBuildText("alice@example.com", []string{"bob@example.com"}, "Att Test", "See attachment")
|
||||
att := EmailAttachment{Filename: "data.txt", ContentType: "text/plain", Data: []byte("file content")}
|
||||
msg = EmailWithAttachment(msg, att)
|
||||
|
||||
if err := SMTPSend(client, msg); err != nil {
|
||||
t.Fatalf("SMTPSend with attachment: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtractAddr tests the internal extractAddr helper.
|
||||
func TestExtractAddr(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"Alice <alice@example.com>", "alice@example.com"},
|
||||
{"bob@example.com", "bob@example.com"},
|
||||
{" carol@example.com ", "carol@example.com"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := extractAddr(c.input)
|
||||
if got != c.want {
|
||||
t.Errorf("extractAddr(%q) = %q, want %q", c.input, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildMIME tests the MIME builder with different body combinations.
|
||||
func TestBuildMIME(t *testing.T) {
|
||||
t.Run("solo texto plano", func(t *testing.T) {
|
||||
msg := EmailBuildText("a@a.com", []string{"b@b.com"}, "Subj", "Hello plain")
|
||||
body, err := buildMIME(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("buildMIME: %v", err)
|
||||
}
|
||||
s := string(body)
|
||||
if !strings.Contains(s, "text/plain") {
|
||||
t.Errorf("expected text/plain in MIME, got:\n%s", s)
|
||||
}
|
||||
if !strings.Contains(s, "Hello plain") {
|
||||
t.Errorf("expected body text in MIME, got:\n%s", s)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("solo html", func(t *testing.T) {
|
||||
msg := EmailBuildHTML("a@a.com", []string{"b@b.com"}, "Subj", "<b>Bold</b>")
|
||||
body, err := buildMIME(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("buildMIME: %v", err)
|
||||
}
|
||||
s := string(body)
|
||||
if !strings.Contains(s, "text/html") {
|
||||
t.Errorf("expected text/html in MIME, got:\n%s", s)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multipart con adjunto", func(t *testing.T) {
|
||||
msg := EmailBuildText("a@a.com", []string{"b@b.com"}, "Subj", "body")
|
||||
att := EmailAttachment{Filename: "f.pdf", ContentType: "application/pdf", Data: []byte("pdf")}
|
||||
msg = EmailWithAttachment(msg, att)
|
||||
body, err := buildMIME(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("buildMIME: %v", err)
|
||||
}
|
||||
s := string(body)
|
||||
if !strings.Contains(s, "multipart/mixed") {
|
||||
t.Errorf("expected multipart/mixed in MIME, got:\n%s", s)
|
||||
}
|
||||
if !strings.Contains(s, "f.pdf") {
|
||||
t.Errorf("expected attachment filename in MIME, got:\n%s", s)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user