Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
203 строки
5.7 KiB
Go
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)
|
|
}
|
|
}
|