Files
worker/internal/notify/email_network_test.go
Gleb Tv 2c884c5612
Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
refactor: adopt worker module path
2026-07-13 17:56:12 +03:00

203 строки
5.7 KiB
Go

package notify
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"testing"
"time"
"rocketgit.ru/rsmon/worker/app/models"
)
const (
mailhogSMTPHost = "localhost"
mailhogSMTPPort = 31025
mailhogAPIURL = "http://localhost:38025"
networkDialTimeout = 2 * time.Second
mailhogPollInterval = 100 * time.Millisecond
mailhogPollDeadline = 3 * time.Second
)
// mailhogMessage mirrors the subset of MailHog's /api/v2/messages payload we
// care about in tests: the parsed headers and the rendered body.
type mailhogMessage struct {
Content struct {
Headers map[string][]string `json:"Headers"`
Body string `json:"Body"`
} `json:"Content"`
}
// skipIfMailHogDown skips the test if MailHog's SMTP port is unreachable.
// Tests stay green on dev machines without docker; on CI with docker they
// run for real against the running MailHog container.
func skipIfMailHogDown(t *testing.T) {
t.Helper()
addr := net.JoinHostPort(mailhogSMTPHost, fmt.Sprintf("%d", mailhogSMTPPort))
conn, err := net.DialTimeout("tcp", addr, networkDialTimeout)
if err != nil {
t.Skipf("mailhog not reachable at %s: %v", addr, err)
}
_ = conn.Close()
}
// mailhogMessages fetches the most recent messages from MailHog's HTTP API.
func mailhogMessages(t *testing.T) []mailhogMessage {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, mailhogAPIURL+"/api/v2/messages?limit=50", nil)
if err != nil {
t.Fatalf("build mailhog request: %v", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("query mailhog: %v", err)
}
defer func() { _ = resp.Body.Close() }()
var out struct {
Total int `json:"total"`
Items []mailhogMessage `json:"items"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
t.Fatalf("decode mailhog response: %v", err)
}
return out.Items
}
// mailhogDeleteAll clears the MailHog in-memory mailbox so each test starts
// from a known state.
func mailhogDeleteAll(t *testing.T) {
t.Helper()
req, err := http.NewRequest(http.MethodDelete, mailhogAPIURL+"/api/v1/messages", nil)
if err != nil {
t.Fatalf("build mailhog delete: %v", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("delete mailhog messages: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
t.Fatalf("mailhog delete returned %d", resp.StatusCode)
}
}
func TestEmail_Network_SendAndVerify(t *testing.T) {
skipIfMailHogDown(t)
mailhogDeleteAll(t)
port := mailhogSMTPPort
enabled := true
cred := &models.NotificationCredential{
Kind: models.CredentialKindSMTP,
Name: "mailhog-test",
Server: strPtr(mailhogSMTPHost),
Port: &port,
Login: strPtr(""),
FromName: strPtr("RSMon Tests"),
FromAddr: strPtr("tests@rsmon.test"),
Enabled: &enabled,
// MailHog accepts any password; "plain:" prefix keeps GetSecret
// working without the credential encryption key configured.
SecretEnc: "plain:",
}
subject := "rsmon-test-subject-" + time.Now().Format("150405.000")
body := "rsmon-test-body hello mailhog"
to := "to@rsmon.test"
if err := Email(cred, to, subject, body, ""); err != nil {
t.Fatalf("Email returned error: %v", err)
}
deadline := time.Now().Add(mailhogPollDeadline)
var found *mailhogMessage
for time.Now().Before(deadline) {
msgs := mailhogMessages(t)
for i := range msgs {
hsubj := msgs[i].Content.Headers["Subject"]
if len(hsubj) > 0 && strings.Contains(hsubj[0], subject) {
found = &msgs[i]
break
}
}
if found != nil {
break
}
time.Sleep(mailhogPollInterval)
}
if found == nil {
t.Fatalf("message with subject %q not found in MailHog after %s", subject, mailhogPollDeadline)
}
if !strings.Contains(found.Content.Body, body) {
t.Errorf("body mismatch: want contains %q, got %q", body, found.Content.Body)
}
if hfrom := found.Content.Headers["From"]; len(hfrom) == 0 || !strings.Contains(hfrom[0], "tests@rsmon.test") {
t.Errorf("from mismatch: want contains tests@rsmon.test, got %v", hfrom)
}
if hto := found.Content.Headers["To"]; len(hto) == 0 || !strings.Contains(hto[0], to) {
t.Errorf("to mismatch: want contains %s, got %v", to, hto)
}
}
func TestEmail_Network_HTMLAlternative(t *testing.T) {
skipIfMailHogDown(t)
mailhogDeleteAll(t)
port := mailhogSMTPPort
enabled := true
cred := &models.NotificationCredential{
Kind: models.CredentialKindSMTP,
Name: "mailhog-html",
Server: strPtr(mailhogSMTPHost),
Port: &port,
Login: strPtr(""),
FromAddr: strPtr("tests@rsmon.test"),
Enabled: &enabled,
SecretEnc: "plain:",
}
subject := "rsmon-html-" + time.Now().Format("150405.000")
bodyText := "plain text body"
bodyHTML := "<p>html <b>body</b></p>"
if err := Email(cred, "to@rsmon.test", subject, bodyText, bodyHTML); err != nil {
t.Fatalf("Email returned error: %v", err)
}
deadline := time.Now().Add(mailhogPollDeadline)
var found *mailhogMessage
for time.Now().Before(deadline) {
msgs := mailhogMessages(t)
for i := range msgs {
hsubj := msgs[i].Content.Headers["Subject"]
if len(hsubj) > 0 && strings.Contains(hsubj[0], subject) {
found = &msgs[i]
break
}
}
if found != nil {
break
}
time.Sleep(mailhogPollInterval)
}
if found == nil {
t.Fatalf("html message with subject %q not found in MailHog after %s", subject, mailhogPollDeadline)
}
body := found.Content.Body
if !strings.Contains(body, bodyText) {
t.Errorf("text part missing: want contains %q in %q", bodyText, body)
}
if !strings.Contains(body, bodyHTML) {
t.Errorf("html part missing: want contains %q in %q", bodyHTML, body)
}
}