yao/openapi/tests/setting/smtp_test.go
Max 13e16c7099 feat(smtp): add SMTP configuration and management endpoints
- Introduced new endpoints for managing SMTP settings, including GET, PUT, and POST methods for retrieving, updating, and testing SMTP configurations.
- Added data structures for SMTP presets and configuration, enhancing the OpenAPI settings to support email functionality.
- Organized routing under a new /smtp group for better endpoint management.
2026-04-29 12:21:19 +08:00

431 lines
13 KiB
Go

package setting_test
import (
"bytes"
"encoding/json"
"net/http"
"os"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/openapi/tests/testutils"
)
// ---------------------------------------------------------------------------
// Functional tests (system:root token)
// ---------------------------------------------------------------------------
func TestSmtpGet(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
req, err := http.NewRequest("GET", serverURL+baseURL()+"/setting/smtp", nil)
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if !assert.NoError(t, err) || !assert.NotNil(t, resp) {
return
}
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&body)
assert.NoError(t, err)
assert.Contains(t, body, "presets")
assert.Contains(t, body, "config")
presets, ok := body["presets"].([]interface{})
assert.True(t, ok)
assert.Equal(t, 6, len(presets), "should have 6 en-us presets: gmail, yahoo, sendgrid, mailgun, ses, custom")
config, ok := body["config"].(map[string]interface{})
assert.True(t, ok)
assert.Equal(t, false, config["enabled"])
assert.Equal(t, "unconfigured", config["status"])
assert.Equal(t, "gmail", config["preset_key"], "default preset for en-us should be gmail")
assert.Equal(t, "", config["password"], "password should be empty when unconfigured")
}
func TestSmtpGetZhCN(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
req, err := http.NewRequest("GET", serverURL+baseURL()+"/setting/smtp?locale=zh-cn", nil)
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if !assert.NoError(t, err) || !assert.NotNil(t, resp) {
return
}
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
presets, ok := body["presets"].([]interface{})
assert.True(t, ok)
assert.Equal(t, 6, len(presets), "should have 6 zh-cn presets: tencent, feishu, aliyun, qq, netease163, custom")
first, _ := presets[0].(map[string]interface{})
assert.Equal(t, "tencent", first["key"])
config, _ := body["config"].(map[string]interface{})
assert.Equal(t, "tencent", config["preset_key"], "default preset for zh-cn should be tencent")
}
func TestSmtpGetUnauthenticated(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
req, err := http.NewRequest("GET", serverURL+baseURL()+"/setting/smtp", nil)
assert.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
func TestSmtpUpdate(t *testing.T) {
host := os.Getenv("RELIABLE_SMTP_HOST")
port := os.Getenv("RELIABLE_SMTP_PORT")
user := os.Getenv("RELIABLE_SMTP_USERNAME")
pass := os.Getenv("RELIABLE_SMTP_PASSWORD")
if host == "" || user == "" || pass == "" {
host = os.Getenv("SMTP_HOST")
port = os.Getenv("SMTP_PORT")
user = os.Getenv("SMTP_USERNAME")
pass = os.Getenv("SMTP_PASSWORD")
}
if host == "" || user == "" || pass == "" {
t.Skip("SMTP credentials not set, skipping")
}
portNum := 465
if port != "" {
if p, err := strconv.Atoi(port); err == nil {
portNum = p
}
}
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
payload := map[string]interface{}{
"preset_key": "custom",
"host": host,
"port": portNum,
"encryption": "ssl",
"username": user,
"password": pass,
"from_name": "Test Sender",
"from_email": user,
}
raw, _ := json.Marshal(payload)
req, err := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
assert.Equal(t, host, body["host"])
assert.Equal(t, user, body["username"])
maskedPwd, _ := body["password"].(string)
assert.True(t, strings.Contains(maskedPwd, "..."), "password should be masked: got %s", maskedPwd)
// GET should also return masked password
req2, _ := http.NewRequest("GET", serverURL+baseURL()+"/setting/smtp", nil)
req2.Header.Set("Authorization", "Bearer "+token)
resp2, err := http.DefaultClient.Do(req2)
assert.NoError(t, err)
defer resp2.Body.Close()
var getData map[string]interface{}
json.NewDecoder(resp2.Body).Decode(&getData)
config, _ := getData["config"].(map[string]interface{})
getMasked, _ := config["password"].(string)
assert.True(t, strings.Contains(getMasked, "..."), "GET should return masked password")
}
func TestSmtpUpdateValidationFailure(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
payload := map[string]interface{}{
"preset_key": "gmail",
"host": "smtp.gmail.com",
"port": 465,
"encryption": "ssl",
"username": "fake@gmail.com",
"password": "wrong-password",
"from_name": "Test",
"from_email": "fake@gmail.com",
}
raw, _ := json.Marshal(payload)
req, err := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "should reject invalid SMTP credentials")
}
func TestSmtpUpdateKeepPassword(t *testing.T) {
host := os.Getenv("RELIABLE_SMTP_HOST")
port := os.Getenv("RELIABLE_SMTP_PORT")
user := os.Getenv("RELIABLE_SMTP_USERNAME")
pass := os.Getenv("RELIABLE_SMTP_PASSWORD")
if host == "" || user == "" || pass == "" {
host = os.Getenv("SMTP_HOST")
port = os.Getenv("SMTP_PORT")
user = os.Getenv("SMTP_USERNAME")
pass = os.Getenv("SMTP_PASSWORD")
}
if host == "" || user == "" || pass == "" {
t.Skip("SMTP credentials not set, skipping")
}
portNum := 465
if port != "" {
if p, err := strconv.Atoi(port); err == nil {
portNum = p
}
}
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
// First save with password
payload1 := map[string]interface{}{
"preset_key": "custom",
"host": host,
"port": portNum,
"encryption": "ssl",
"username": user,
"password": pass,
"from_name": "Test",
"from_email": user,
}
raw, _ := json.Marshal(payload1)
req, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
resp.Body.Close()
// Update without password — should keep original and re-validate with existing password
payload2 := map[string]interface{}{
"preset_key": "custom",
"host": host,
"port": portNum,
"encryption": "ssl",
"username": user,
"password": "",
"from_name": "Updated",
"from_email": user,
}
raw, _ = json.Marshal(payload2)
req2, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
req2.Header.Set("Authorization", "Bearer "+token)
req2.Header.Set("Content-Type", "application/json")
resp2, err := http.DefaultClient.Do(req2)
assert.NoError(t, err)
defer resp2.Body.Close()
assert.Equal(t, http.StatusOK, resp2.StatusCode)
var body map[string]interface{}
json.NewDecoder(resp2.Body).Decode(&body)
assert.Equal(t, "Updated", body["from_name"])
keepMasked, _ := body["password"].(string)
assert.True(t, strings.Contains(keepMasked, "..."), "password should be masked (kept original)")
}
func TestSmtpToggle(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
// Save config first
savePayload := map[string]interface{}{
"preset_key": "gmail",
"host": "smtp.gmail.com",
"port": 465,
"encryption": "ssl",
"username": "test@gmail.com",
"password": "test-pass",
"from_name": "Test",
"from_email": "test@gmail.com",
}
raw, _ := json.Marshal(savePayload)
req, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
resp.Body.Close()
// Enable
enablePayload := map[string]interface{}{"enabled": true}
raw, _ = json.Marshal(enablePayload)
req2, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp/toggle", bytes.NewReader(raw))
req2.Header.Set("Authorization", "Bearer "+token)
req2.Header.Set("Content-Type", "application/json")
resp2, err := http.DefaultClient.Do(req2)
assert.NoError(t, err)
defer resp2.Body.Close()
assert.Equal(t, http.StatusOK, resp2.StatusCode)
var body map[string]interface{}
json.NewDecoder(resp2.Body).Decode(&body)
assert.Equal(t, true, body["enabled"])
// Disable
disablePayload := map[string]interface{}{"enabled": false}
raw, _ = json.Marshal(disablePayload)
req3, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp/toggle", bytes.NewReader(raw))
req3.Header.Set("Authorization", "Bearer "+token)
req3.Header.Set("Content-Type", "application/json")
resp3, err := http.DefaultClient.Do(req3)
assert.NoError(t, err)
defer resp3.Body.Close()
assert.Equal(t, http.StatusOK, resp3.StatusCode)
var body2 map[string]interface{}
json.NewDecoder(resp3.Body).Decode(&body2)
assert.Equal(t, false, body2["enabled"])
assert.Equal(t, "unconfigured", body2["status"])
}
func TestSmtpTest(t *testing.T) {
host := os.Getenv("RELIABLE_SMTP_HOST")
port := os.Getenv("RELIABLE_SMTP_PORT")
user := os.Getenv("RELIABLE_SMTP_USERNAME")
pass := os.Getenv("RELIABLE_SMTP_PASSWORD")
if host == "" || user == "" || pass == "" {
host = os.Getenv("SMTP_HOST")
port = os.Getenv("SMTP_PORT")
user = os.Getenv("SMTP_USERNAME")
pass = os.Getenv("SMTP_PASSWORD")
}
if host == "" || user == "" || pass == "" {
t.Skip("RELIABLE_SMTP_* or SMTP_* env not set, skipping SMTP test")
}
toEmail := os.Getenv("SMTP_TEST_TO")
if toEmail == "" {
toEmail = user
}
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
portNum := 465
if port != "" {
if p, err := strconv.Atoi(port); err == nil {
portNum = p
}
}
savePayload := map[string]interface{}{
"preset_key": "custom",
"host": host,
"port": portNum,
"encryption": "ssl",
"username": user,
"password": pass,
"from_name": "Yao SMTP Test",
"from_email": user,
}
raw, _ := json.Marshal(savePayload)
req, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
resp.Body.Close()
testPayload := map[string]interface{}{"to_email": toEmail}
raw, _ = json.Marshal(testPayload)
req2, err := http.NewRequest("POST", serverURL+baseURL()+"/setting/smtp/test", bytes.NewReader(raw))
assert.NoError(t, err)
req2.Header.Set("Authorization", "Bearer "+token)
req2.Header.Set("Content-Type", "application/json")
resp2, err := http.DefaultClient.Do(req2)
assert.NoError(t, err)
defer resp2.Body.Close()
assert.Equal(t, http.StatusOK, resp2.StatusCode)
var body map[string]interface{}
json.NewDecoder(resp2.Body).Decode(&body)
t.Logf("SMTP test result: %+v", body)
assert.Equal(t, true, body["success"])
}
// ---------------------------------------------------------------------------
// ACL permission tests
// ---------------------------------------------------------------------------
func TestSmtpACL_ReadOnlyScopeCannotWrite(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
readToken := obtainRestrictedToken(t, serverURL, "setting:smtp:read:all")
// GET should work
req, _ := http.NewRequest("GET", serverURL+baseURL()+"/setting/smtp", nil)
req.Header.Set("Authorization", "Bearer "+readToken)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "read-only scope should allow GET")
// PUT should be denied
payload := map[string]interface{}{
"preset_key": "gmail",
"host": "smtp.gmail.com",
"port": 465,
"encryption": "ssl",
"username": "test@gmail.com",
"password": "test-pass",
"from_name": "Test",
"from_email": "test@gmail.com",
}
raw, _ := json.Marshal(payload)
req2, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
req2.Header.Set("Authorization", "Bearer "+readToken)
req2.Header.Set("Content-Type", "application/json")
resp2, err := http.DefaultClient.Do(req2)
assert.NoError(t, err)
defer resp2.Body.Close()
assert.Equal(t, http.StatusForbidden, resp2.StatusCode, "read-only scope should deny PUT")
}