feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
202
internal/notify/email_network_test.go
Обычный файл
202
internal/notify/email_network_test.go
Обычный файл
@@ -0,0 +1,202 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/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)
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user