167 lines
4.7 KiB
Go
167 lines
4.7 KiB
Go
package notify
|
||
|
||
import (
|
||
"bufio"
|
||
"context"
|
||
"encoding/base64"
|
||
"net"
|
||
"strconv"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"dana/internal/domain"
|
||
)
|
||
|
||
// fakeSMTP — минимальный SMTP-сервер без TLS и авторизации: принимает одно письмо.
|
||
func fakeSMTP(t *testing.T) (host string, port int, got <-chan string) {
|
||
t.Helper()
|
||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() { ln.Close() })
|
||
|
||
ch := make(chan string, 1)
|
||
go func() {
|
||
conn, err := ln.Accept()
|
||
if err != nil {
|
||
return
|
||
}
|
||
defer conn.Close()
|
||
r := bufio.NewReader(conn)
|
||
w := func(s string) { conn.Write([]byte(s + "\r\n")) }
|
||
|
||
w("220 fake")
|
||
var transcript strings.Builder
|
||
inData := false
|
||
for {
|
||
line, err := r.ReadString('\n')
|
||
if err != nil {
|
||
return
|
||
}
|
||
if inData {
|
||
if line == ".\r\n" {
|
||
inData = false
|
||
w("250 queued")
|
||
continue
|
||
}
|
||
transcript.WriteString(line)
|
||
continue
|
||
}
|
||
cmd := strings.ToUpper(strings.TrimSpace(line))
|
||
switch {
|
||
case strings.HasPrefix(cmd, "EHLO"), strings.HasPrefix(cmd, "HELO"):
|
||
w("250 fake")
|
||
case strings.HasPrefix(cmd, "MAIL FROM"), strings.HasPrefix(cmd, "RCPT TO"):
|
||
transcript.WriteString(strings.TrimSpace(line) + "\n")
|
||
w("250 ok")
|
||
case cmd == "DATA":
|
||
inData = true
|
||
w("354 go")
|
||
case cmd == "QUIT":
|
||
w("221 bye")
|
||
ch <- transcript.String()
|
||
return
|
||
default:
|
||
w("250 ok")
|
||
}
|
||
}
|
||
}()
|
||
|
||
addr := ln.Addr().(*net.TCPAddr)
|
||
return "127.0.0.1", addr.Port, ch
|
||
}
|
||
|
||
func TestEmailLeadCreated(t *testing.T) {
|
||
host, port, got := fakeSMTP(t)
|
||
e := &Email{Host: host, Port: port, From: "DANA сайт <site@example.ru>", To: []string{"admin@example.ru"}, SiteURL: "https://danamedical.ru"}
|
||
|
||
lead := domain.LeadNotice{
|
||
ID: 42,
|
||
FormTitle: "Запись на приём",
|
||
Name: "Иван\r\nBcc: evil@example.com",
|
||
Phone: "+7 (928) 000-00-99",
|
||
Service: "Консультация педиатра",
|
||
Extra: map[string]any{"date": "2026-09-20", "time": "Утро, 08:00–12:00"},
|
||
Message: "Тестовая заявка",
|
||
PageURL: "/services/detskaya-klinika/priem-pediatra/",
|
||
CreatedAt: time.Date(2026, 9, 15, 9, 30, 0, 0, time.UTC),
|
||
}
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
if err := e.LeadCreated(ctx, lead); err != nil {
|
||
t.Fatalf("LeadCreated: %v", err)
|
||
}
|
||
|
||
var transcript string
|
||
select {
|
||
case transcript = <-got:
|
||
case <-time.After(5 * time.Second):
|
||
t.Fatal("fake SMTP did not receive message")
|
||
}
|
||
|
||
if !strings.Contains(transcript, "MAIL FROM:<site@example.ru>") || !strings.Contains(transcript, "RCPT TO:<admin@example.ru>") {
|
||
t.Fatalf("envelope not as expected:\n%s", transcript)
|
||
}
|
||
// перевод строки в имени не должен стать заголовком письма
|
||
if strings.Contains(transcript, "\r\nBcc:") {
|
||
t.Fatalf("header injection:\n%s", transcript)
|
||
}
|
||
|
||
body := decodeBody(t, transcript)
|
||
for _, want := range []string{
|
||
"Новая заявка № 42",
|
||
"Время: 15.09.2026 12:30",
|
||
"Форма: Запись на приём",
|
||
"Имя: Иван Bcc: evil@example.com",
|
||
"Телефон: +7 (928) 000-00-99",
|
||
"Услуга: Консультация педиатра",
|
||
"Желаемая дата: 2026-09-20",
|
||
"Удобное время: Утро, 08:00–12:00",
|
||
"Тестовая заявка",
|
||
"Все заявки: https://danamedical.ru/admin/",
|
||
} {
|
||
if !strings.Contains(body, want) {
|
||
t.Errorf("body has no %q:\n%s", want, body)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestFormRecipientsOverrideDefault(t *testing.T) {
|
||
host, port, got := fakeSMTP(t)
|
||
e := &Email{Host: host, Port: port, From: "site@example.ru", To: []string{"default@example.ru"}}
|
||
lead := domain.LeadNotice{ID: 1, Recipients: []string{"doctors@example.ru", "не адрес"}}
|
||
|
||
if err := e.LeadCreated(context.Background(), lead); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
transcript := <-got
|
||
if !strings.Contains(transcript, "RCPT TO:<doctors@example.ru>") || strings.Contains(transcript, "default@example.ru") {
|
||
t.Fatalf("recipients of the form must replace defaults:\n%s", transcript)
|
||
}
|
||
}
|
||
|
||
func TestNoRecipients(t *testing.T) {
|
||
e := &Email{Host: "127.0.0.1", Port: 1, From: "site@example.ru"}
|
||
if err := e.LeadCreated(context.Background(), domain.LeadNotice{ID: 1}); err != errNoRecipients {
|
||
t.Fatalf("want errNoRecipients, got %v", err)
|
||
}
|
||
}
|
||
|
||
func decodeBody(t *testing.T, transcript string) string {
|
||
t.Helper()
|
||
parts := strings.SplitN(transcript, "\r\n\r\n", 2)
|
||
if len(parts) != 2 {
|
||
t.Fatalf("no body separator:\n%s", transcript)
|
||
}
|
||
raw, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(parts[1], "\r\n", ""))
|
||
if err != nil {
|
||
t.Fatalf("decode body: %v", err)
|
||
}
|
||
return string(raw)
|
||
}
|
||
|
||
var _ = strconv.Itoa
|