Files
jules/chat/email/email_test.go
T

289 lines
6.9 KiB
Go

package email
import (
"bufio"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
"net"
"testing"
"time"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Generate self-signed certificate for TLS testing
func generateTLSCert() tls.Certificate {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
DNSNames: []string{"localhost", "127.0.0.1"},
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
panic(err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
panic(err)
}
return tlsCert
}
// startFakeSMTPServer creates a TLS-enabled fake SMTP server
func startFakeSMTPServer(t *testing.T, handler func(conn net.Conn)) string {
t.Helper()
cert := generateTLSCert()
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
}
listener, err := tls.Listen("tcp", "127.0.0.1:0", tlsConfig)
require.NoError(t, err)
t.Cleanup(func() { listener.Close() })
go func() {
conn, err := listener.Accept()
if err != nil {
return
}
defer conn.Close()
handler(conn)
}()
_, port, _ := net.SplitHostPort(listener.Addr().String())
return port
}
// createTestClient is a helper to create a Client with TLS skip verification
func createTestClient(smtpPort, username, password string) *Client {
return &Client{
smtpAddr: fmt.Sprintf("127.0.0.1:%s", smtpPort),
imapAddr: "imap.test.com:993",
username: username,
password: password,
fromAddr: "user@test.com",
pollInterval: 10 * time.Second,
tlsConfig: &tls.Config{InsecureSkipVerify: true},
}
}
func TestNew(t *testing.T) {
t.Run("with defaults", func(t *testing.T) {
c, err := New(Config{
SMTPHost: "smtp.test.com",
IMAPHost: "imap.test.com",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
require.NoError(t, err)
assert.Equal(t, "smtp.test.com:465", c.smtpAddr)
assert.Equal(t, "imap.test.com:993", c.imapAddr)
assert.Equal(t, "user@test.com", c.username)
assert.Equal(t, 10*time.Second, c.pollInterval)
})
t.Run("custom ports", func(t *testing.T) {
c, err := New(Config{
SMTPHost: "smtp.test.com",
SMTPPort: "587",
IMAPHost: "imap.test.com",
IMAPPort: "143",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
require.NoError(t, err)
assert.Equal(t, "smtp.test.com:587", c.smtpAddr)
assert.Equal(t, "imap.test.com:143", c.imapAddr)
})
}
func TestSend_EmptyRecipient(t *testing.T) {
c, _ := New(Config{
SMTPHost: "smtp.test.com",
IMAPHost: "imap.test.com",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
err := c.Send(context.Background(), "", "hello")
require.Error(t, err)
assert.Contains(t, err.Error(), "empty recipient")
}
func TestSend_SMTPServerDown(t *testing.T) {
c, _ := New(Config{
SMTPHost: "127.0.0.1",
SMTPPort: "25999",
IMAPHost: "imap.test.com",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
err := c.Send(context.Background(), "to@test.com", "hello")
require.Error(t, err)
assert.Contains(t, err.Error(), "smtp connect")
}
func TestSend_Success(t *testing.T) {
port := startFakeSMTPServer(t, func(conn net.Conn) {
reader := bufio.NewReader(conn)
// Greeting
fmt.Fprintf(conn, "220 smtp.example.com ESMTP\r\n")
// Read EHLO
reader.ReadString('\n')
fmt.Fprintf(conn, "250-smtp.example.com\r\n")
fmt.Fprintf(conn, "250 AUTH PLAIN\r\n")
// Read AUTH
reader.ReadString('\n')
fmt.Fprintf(conn, "235 2.7.0 Authentication successful\r\n")
// Read MAIL FROM
reader.ReadString('\n')
fmt.Fprintf(conn, "250 2.1.0 Ok\r\n")
// Read RCPT TO
reader.ReadString('\n')
fmt.Fprintf(conn, "250 2.1.5 Ok\r\n")
// Read DATA
reader.ReadString('\n')
fmt.Fprintf(conn, "354 End data with <CR><LF>.<CR><LF>\r\n")
// Read message body (ending with \r\n.\r\n)
reader.ReadString('\n')
reader.ReadString('\n')
fmt.Fprintf(conn, "250 2.0.0 Ok: queued\r\n")
})
c := createTestClient(port, "user@test.com", "pass")
err := c.Send(context.Background(), "to@test.com", "Hello World")
require.NoError(t, err)
}
func TestSend_AuthFailure(t *testing.T) {
port := startFakeSMTPServer(t, func(conn net.Conn) {
reader := bufio.NewReader(conn)
fmt.Fprintf(conn, "220 smtp.example.com ESMTP\r\n")
reader.ReadString('\n')
fmt.Fprintf(conn, "250-smtp.example.com\r\n")
fmt.Fprintf(conn, "250 AUTH PLAIN\r\n")
reader.ReadString('\n')
fmt.Fprintf(conn, "535 5.7.8 Authentication failed\r\n")
})
c := createTestClient(port, "user@test.com", "wrong")
err := c.Send(context.Background(), "to@test.com", "Hello")
require.Error(t, err)
assert.Contains(t, err.Error(), "smtp auth")
}
func TestReceive_ContextCancel(t *testing.T) {
c, _ := New(Config{
SMTPHost: "smtp.test.com",
IMAPHost: "127.0.0.1",
IMAPPort: "25998",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
ctx, cancel := context.WithCancel(context.Background())
ch := c.Receive(ctx)
cancel()
// Should close quickly
select {
case _, ok := <-ch:
assert.False(t, ok)
case <-time.After(time.Second):
t.Fatal("channel not closed after cancel")
}
}
func TestReceive_ConnectionFailure(t *testing.T) {
c, _ := New(Config{
SMTPHost: "smtp.test.com",
IMAPHost: "127.0.0.1",
IMAPPort: "25999",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ch := c.Receive(ctx)
select {
case msg := <-ch:
t.Fatalf("unexpected message: %+v", msg)
case <-time.After(2 * time.Second):
// Expected - connection failed, channel closed
}
}
func TestExtractFrom(t *testing.T) {
t.Run("nil envelope", func(t *testing.T) {
assert.Equal(t, "", extractFrom(nil))
})
t.Run("with data", func(t *testing.T) {
env := &imap.Envelope{
From: []imap.Address{
{
Name: "John Doe",
Mailbox: "john",
Host: "example.com",
},
},
}
assert.Equal(t, "john@example.com", extractFrom(env))
})
}
func TestExtractBody(t *testing.T) {
t.Run("empty sections", func(t *testing.T) {
msg := &imapclient.FetchMessageBuffer{}
assert.Equal(t, "", extractBody(msg))
})
t.Run("with data", func(t *testing.T) {
msg := &imapclient.FetchMessageBuffer{
BodySection: []imapclient.FetchBodySectionBuffer{{
Section: &imap.FetchItemBodySection{},
Bytes: []byte("hello world"),
}},
}
assert.Equal(t, "hello world", extractBody(msg))
})
}