- 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.
431 lines
13 KiB
Go
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")
|
|
}
|