Files
mailgoserver/internal/webui/webui_test.go
T

250 lines
8.3 KiB
Go

package webui
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"gopkg.in/ini.v1"
"mailgoserver/internal/acmecert"
"mailgoserver/internal/db"
"mailgoserver/internal/dkim"
"mailgoserver/internal/mailstore"
"mailgoserver/internal/toolbox"
)
func newTestApp(t *testing.T) *App {
t.Helper()
dir := t.TempDir()
dbPath := filepath.Join(dir, "test.db")
database, err := db.Open(dbPath)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { database.Close() })
// Seed one of everything so every page has real data to render.
domainID, err := database.CreateDomain("example.com")
if err != nil {
t.Fatal(err)
}
hash, _ := db.HashPassword("testpass123")
senderID, err := database.CreateSender("test@example.com", hash, domainID, true, true)
if err != nil {
t.Fatal(err)
}
_, err = database.CreateWhitelistedIP("127.0.0.1", domainID, false)
if err != nil {
t.Fatal(err)
}
dkimMgr := dkim.New(database, 1024)
ok, err := dkimMgr.GenerateDKIMKeypair("example.com", "sel1", false)
if err != nil || !ok {
t.Fatalf("dkim gen: ok=%v err=%v", ok, err)
}
key, err := database.GetActiveDKIMKeyByDomainID(domainID)
if err != nil || key == nil {
t.Fatalf("get active dkim key: %v %v", key, err)
}
logID, err := database.InsertEmailLog(db.EmailLog{
MessageID: "abc123@example.com", Timestamp: time.Now(), PeerIP: "127.0.0.1",
MailFrom: "test@example.com", ToAddress: "rcpt@example.org", Subject: "hi",
EmailHeaders: "From: test@example.com\nTo: rcpt@example.org", MessageBody: "hello",
Status: "relayed", DKIMSigned: true, Username: "test@example.com",
})
if err != nil {
t.Fatal(err)
}
if err := database.InsertEmailRecipientLog(db.EmailRecipientLog{EmailLogID: logID, Recipient: "rcpt@example.org", RecipientType: "to", Status: "success"}); err != nil {
t.Fatal(err)
}
attFile := filepath.Join(dir, "att.txt")
os.WriteFile(attFile, []byte("attachment data"), 0o644)
if err := database.InsertEmailAttachment(db.EmailAttachment{EmailLogID: logID, Filename: "att.txt", ContentType: "text/plain", FilePath: attFile, Size: 15}); err != nil {
t.Fatal(err)
}
if err := database.LogAuthAttempt("sender", "test@example.com", "127.0.0.1", true, "ok"); err != nil {
t.Fatal(err)
}
mstore := mailstore.New(database, mailstore.GenerateDEK(), filepath.Join(dir, "mailstore"))
mdek := mailstore.GenerateDEK()
mwrapped, mnonce, err := mstore.WrapDEK(mdek)
if err != nil {
t.Fatal(err)
}
mboxID, err := database.CreateMailbox("inbox@example.com", hash, domainID, 5*1024*1024*1024, mwrapped, mnonce)
if err != nil {
t.Fatal(err)
}
cfg := ini.Empty()
serverSec, _ := cfg.NewSection("Server")
serverSec.NewKey("smtp_port", "4025")
serverSec.NewKey("smtp_tls_port", "40465")
serverSec.NewKey("web_http_port", "5000")
serverSec.NewKey("web_https_port", "5001")
serverSec.NewKey("bind_ip", "0.0.0.0")
serverSec.NewKey("time_zone", "UTC")
serverSec.NewKey("hostname", "mail.example.com")
serverSec.NewKey("helo_hostname", "mail.example.com")
serverSec.NewKey("server_banner", "")
imapSec, _ := cfg.NewSection("IMAP")
imapSec.NewKey("imap_port", "1143")
imapSec.NewKey("imap_tls_port", "1993")
authSec, _ := cfg.NewSection("Auth")
authSec.NewKey("enforce_admin_mfa", "false")
authSec.NewKey("enforce_mailbox_mfa", "false")
rspamdSec, _ := cfg.NewSection("Rspamd")
rspamdSec.NewKey("enabled", "false")
rspamdSec.NewKey("url", "http://127.0.0.1:11333")
rspamdSec.NewKey("reject_score", "15")
dbSec, _ := cfg.NewSection("Database")
dbSec.NewKey("database_url", "sqlite:///server_data/smtp_server.db")
logSec, _ := cfg.NewSection("Logging")
logSec.NewKey("log_level", "INFO")
logSec.NewKey("hide_info_aiosmtpd", "true")
relaySec, _ := cfg.NewSection("Relay")
relaySec.NewKey("relay_timeout", "30")
tlsSec, _ := cfg.NewSection("TLS")
tlsSec.NewKey("tls_cert_file", "ssl_certs/server.crt")
tlsSec.NewKey("tls_key_file", "ssl_certs/server.key")
dkimSec, _ := cfg.NewSection("DKIM")
dkimSec.NewKey("dkim_key_size", "2048")
dkimSec.NewKey("spf_server_ip", "192.168.1.1")
attSec, _ := cfg.NewSection("Attachments")
attSec.NewKey("attachments_path", filepath.Join(dir, "attachments"))
mailstoreSec, _ := cfg.NewSection("Mailstore")
mailstoreSec.NewKey("app_password_min_length", "25")
mailstoreSec.NewKey("spam_reject_score", "5")
configPath := filepath.Join(dir, "settings.ini")
cfg.SaveTo(configPath)
acmeMgr := acmecert.New(cfg, filepath.Join(dir, "server.crt"), filepath.Join(dir, "server.key"), filepath.Join(dir, "acme"), nil, toolbox.GetLogger("test"))
app, err := New(database, dkimMgr, mstore, acmeMgr, cfg, configPath, toolbox.GetLogger("test"), func() bool { return true })
if err != nil {
t.Fatalf("New: %v", err)
}
_ = senderID
_ = key
_ = mboxID
return app
}
// loginSession creates a fully-verified admin session (no MFA enrolled) and returns
// its cookie, for tests that need to hit routes behind requireAuth.
func loginSession(t *testing.T, app *App) *http.Cookie {
t.Helper()
hash, err := db.HashPassword("test-password-123!")
if err != nil {
t.Fatal(err)
}
userID, err := app.DB.CreateAdminUser("test-admin", hash, false)
if err != nil {
t.Fatal(err)
}
token, err := app.DB.CreateSession(userID, true, time.Hour)
if err != nil {
t.Fatal(err)
}
return &http.Cookie{Name: sessionCookieName, Value: token}
}
func TestAllPagesRender(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
cookie := loginSession(t, app)
domains, _ := app.DB.ListDomains()
senders, _ := app.DB.ListSenders()
mailboxes, _ := app.DB.ListMailboxes()
ips, _ := app.DB.ListWhitelistedIPs()
keys, _ := app.DB.ListActiveDKIMKeysWithDomain()
logs, _ := app.DB.ListEmailLogsPage(0, 10)
if len(domains) == 0 || len(senders) == 0 || len(mailboxes) == 0 || len(ips) == 0 || len(keys) == 0 || len(logs) == 0 {
t.Fatalf("seed data missing: domains=%d senders=%d mailboxes=%d ips=%d keys=%d logs=%d", len(domains), len(senders), len(mailboxes), len(ips), len(keys), len(logs))
}
pagesToCheck := []string{
"/",
"/account",
"/domains", "/domains/add", "/domains/" + itoa(domains[0].ID) + "/edit",
"/senders", "/senders/add", "/senders/" + itoa(senders[0].ID) + "/edit",
"/mailboxes", "/mailboxes/add", "/mailboxes/" + itoa(mailboxes[0].ID) + "/edit", "/mailboxes/" + itoa(mailboxes[0].ID) + "/apppasswords", "/mailboxes/" + itoa(mailboxes[0].ID) + "/aliases",
"/mailboxes/" + itoa(mailboxes[0].ID) + "/lists", "/mailboxes/" + itoa(mailboxes[0].ID) + "/rules",
"/ips", "/ips/add", "/ips/" + itoa(ips[0].ID) + "/edit",
"/dkim", "/dkim/" + itoa(keys[0].ID) + "/edit",
"/logs", "/logs?type=emails", "/logs?type=auth",
"/settings",
"/letsencrypt",
"/msg/content/" + itoa(logs[0].ID),
"/admins", "/admins/add",
}
for _, path := range pagesToCheck {
req := httptest.NewRequest(http.MethodGet, Prefix+path, nil)
req.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("GET %s: status %d, body: %s", path, rec.Code, rec.Body.String())
}
}
}
func TestUnauthenticatedRequestsRedirectToLogin(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
req := httptest.NewRequest(http.MethodGet, Prefix+"/domains", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("expected redirect to login, got status %d", rec.Code)
}
loc := rec.Header().Get("Location")
if !strings.HasPrefix(loc, Prefix+"/login") {
t.Fatalf("expected redirect to login page, got %q", loc)
}
}
func TestFirstLoginForcedBeforeDashboard(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
hash, err := db.HashPassword("Password123!")
if err != nil {
t.Fatal(err)
}
userID, err := app.DB.CreateAdminUser("admin", hash, true) // must_change_password
if err != nil {
t.Fatal(err)
}
token, err := app.DB.CreateSession(userID, true, time.Hour)
if err != nil {
t.Fatal(err)
}
cookie := &http.Cookie{Name: sessionCookieName, Value: token}
req := httptest.NewRequest(http.MethodGet, Prefix+"/domains", nil)
req.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusFound || rec.Header().Get("Location") != Prefix+"/first-login" {
t.Fatalf("expected redirect to /first-login, got status %d location %q", rec.Code, rec.Header().Get("Location"))
}
}
func itoa(id int64) string {
return strconv.FormatInt(id, 10)
}