836 lines
28 KiB
Go
836 lines
28 KiB
Go
package email
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
channelpkg "github.com/gochat/gochat/internal/channel"
|
|
"github.com/gochat/gochat/internal/model"
|
|
)
|
|
|
|
// ============================================================
|
|
// Service tests
|
|
// ============================================================
|
|
|
|
func TestNewService(t *testing.T) {
|
|
svc := NewService()
|
|
assert.NotNil(t, svc)
|
|
assert.NotNil(t, svc.imapListener)
|
|
assert.NotNil(t, svc.smtpSender)
|
|
}
|
|
|
|
func TestService_ValidateIMAPConnection(t *testing.T) {
|
|
svc := NewService()
|
|
// Incomplete config — should error because IMAP address/login/password missing
|
|
err := svc.ValidateIMAPConnection(channelpkg.ChannelConfig{})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "required")
|
|
}
|
|
|
|
func TestService_ValidateSMTPConnection(t *testing.T) {
|
|
svc := NewService()
|
|
err := svc.ValidateSMTPConnection(channelpkg.ChannelConfig{})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "required")
|
|
}
|
|
|
|
func TestService_FetchIMAPMessages_IncompleteConfig(t *testing.T) {
|
|
svc := NewService()
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
_, err := svc.FetchIMAPMessages(context.Background(), inbox, channelpkg.ChannelConfig{})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "incomplete")
|
|
}
|
|
|
|
func TestService_SendViaSMTP_IncompleteConfig(t *testing.T) {
|
|
svc := NewService()
|
|
_, err := svc.SendViaSMTP(channelpkg.ChannelConfig{}, "from@test.com", "to@test.com", "subject", "body")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "incomplete")
|
|
}
|
|
|
|
// ============================================================
|
|
// Address processing helpers
|
|
// ============================================================
|
|
|
|
func TestExtractEmailAddress(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want string
|
|
}{
|
|
{"", ""},
|
|
{"john@example.com", "john@example.com"},
|
|
{"John Doe <john@example.com>", "john@example.com"},
|
|
{`"John Doe" <john@example.com>`, "john@example.com"},
|
|
{"<john@example.com>", "john@example.com"},
|
|
{" john@example.com ", "john@example.com"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := extractEmailAddress(tt.input)
|
|
assert.Equal(t, tt.want, got, "input=%q", tt.input)
|
|
}
|
|
}
|
|
|
|
func TestExtractDisplayName(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want string
|
|
}{
|
|
{"", ""},
|
|
{"john@example.com", ""},
|
|
{"John Doe <john@example.com>", "John Doe"},
|
|
{`"John Doe" <john@example.com>`, "John Doe"},
|
|
{"<john@example.com>", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
got := extractDisplayName(tt.input)
|
|
assert.Equal(t, tt.want, got, "input=%q", tt.input)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Types tests
|
|
// ============================================================
|
|
|
|
func TestConfigInt(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config channelpkg.ChannelConfig
|
|
key string
|
|
def int
|
|
want int
|
|
}{
|
|
{"missing key", channelpkg.ChannelConfig{}, "port", 993, 993},
|
|
{"int value", channelpkg.ChannelConfig{"port": 587}, "port", 993, 587},
|
|
{"int64 value", channelpkg.ChannelConfig{"port": int64(587)}, "port", 993, 587},
|
|
{"float64 value", channelpkg.ChannelConfig{"port": float64(587)}, "port", 993, 587},
|
|
{"string value valid", channelpkg.ChannelConfig{"port": "587"}, "port", 993, 587},
|
|
{"string value invalid", channelpkg.ChannelConfig{"port": "abc"}, "port", 993, 993},
|
|
{"nil value", channelpkg.ChannelConfig{"port": nil}, "port", 993, 993},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := configInt(tt.config, tt.key, tt.def)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHTMLToPlainText(t *testing.T) {
|
|
// htmlToPlainText first replaces entities, then strips tags (adding spaces),
|
|
// then collapses whitespace.
|
|
// Note: < > become < > which stripHTMLTags interprets as tag delimiters.
|
|
tests := []struct {
|
|
name string
|
|
html string
|
|
want string
|
|
}{
|
|
{"empty", "", ""},
|
|
{"plain text", "Hello world", "Hello world"},
|
|
{"basic HTML", "<p>Hello</p>", "Hello"},
|
|
{"nested HTML", "<div><p>Hello</p></div>", "Hello"},
|
|
{"amp entity only", "&", "&"},
|
|
{"with nbsp", " hello", "hello"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := htmlToPlainText(tt.html)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStripHTMLTags(t *testing.T) {
|
|
// stripHTMLTags replaces tags with spaces
|
|
assert.Equal(t, " hello ", stripHTMLTags("<b>hello</b>"))
|
|
assert.Equal(t, " a b ", stripHTMLTags("<div>a</div><span>b</span>"))
|
|
assert.Equal(t, "text", stripHTMLTags("text"))
|
|
}
|
|
|
|
func TestCollapseWhitespace(t *testing.T) {
|
|
assert.Equal(t, "a b c", collapseWhitespace("a b\n\nc"))
|
|
assert.Equal(t, " a b c ", collapseWhitespace(" a b\r\nc "))
|
|
assert.Equal(t, " ", collapseWhitespace(" "))
|
|
assert.Equal(t, "x", collapseWhitespace("x"))
|
|
}
|
|
|
|
func TestLooksLikeHTML(t *testing.T) {
|
|
assert.True(t, looksLikeHTML("<html><body>hi</body></html>"))
|
|
assert.True(t, looksLikeHTML("<p>hi</p>"))
|
|
assert.True(t, looksLikeHTML("<div>hi</div>"))
|
|
assert.True(t, looksLikeHTML("<b>bold</b>"))
|
|
assert.True(t, looksLikeHTML("<br>"))
|
|
assert.False(t, looksLikeHTML("just text"))
|
|
assert.False(t, looksLikeHTML(""))
|
|
}
|
|
|
|
func TestStripContentIDBrackets(t *testing.T) {
|
|
assert.Equal(t, "abc", stripContentIDBrackets("<abc>"))
|
|
assert.Equal(t, "abc", stripContentIDBrackets("abc"))
|
|
assert.Equal(t, "abc", stripContentIDBrackets(" <abc> "))
|
|
assert.Equal(t, "<abc", stripContentIDBrackets("<abc"))
|
|
}
|
|
|
|
func TestExtractDomainFromAddress(t *testing.T) {
|
|
assert.Equal(t, "example.com", extractDomainFromAddress("user@example.com"))
|
|
assert.Equal(t, "", extractDomainFromAddress("invalid"))
|
|
assert.Equal(t, "", extractDomainFromAddress(""))
|
|
}
|
|
|
|
func TestBuildSubjectForReply(t *testing.T) {
|
|
assert.Equal(t, "Re: Your inquiry", buildSubjectForReply(""))
|
|
assert.Equal(t, "Re: Hello", buildSubjectForReply("Hello"))
|
|
assert.Equal(t, "Re: Hello", buildSubjectForReply("Re: Hello"))
|
|
assert.Equal(t, "RE: Hello", buildSubjectForReply("RE: Hello"))
|
|
}
|
|
|
|
func TestExtractThreadIDFromReferences(t *testing.T) {
|
|
assert.Equal(t, "", extractThreadIDFromReferences(""))
|
|
assert.Equal(t, "abc@domain", extractThreadIDFromReferences("<abc@domain>"))
|
|
assert.Equal(t, "abc@domain", extractThreadIDFromReferences("<abc@domain> <def@domain>"))
|
|
}
|
|
|
|
func TestParseMessageIDList(t *testing.T) {
|
|
assert.Nil(t, parseMessageIDList(""))
|
|
assert.Equal(t, []string{"abc"}, parseMessageIDList("<abc>"))
|
|
assert.Equal(t, []string{"a", "b"}, parseMessageIDList("<a> <b>"))
|
|
}
|
|
|
|
func TestGenerateMessageID(t *testing.T) {
|
|
id := generateMessageID("example.com")
|
|
assert.True(t, strings.HasPrefix(id, "<gochat-"))
|
|
assert.True(t, strings.HasSuffix(id, "@example.com>"))
|
|
|
|
idDefault := generateMessageID("")
|
|
assert.True(t, strings.HasSuffix(idDefault, "@gochat.local>"))
|
|
}
|
|
|
|
func TestFormatDateNow(t *testing.T) {
|
|
d := formatDateNow()
|
|
assert.NotEmpty(t, d)
|
|
// Format: "02 Jan 06 15:04 -0700"
|
|
assert.Regexp(t, `^\d{2} \w{3} \d{2} \d{2}:\d{2} [+-]\d{4}$`, d)
|
|
}
|
|
|
|
func TestConvertToIncomingMessage(t *testing.T) {
|
|
inbox := &model.Inbox{Base: model.Base{ID: 10}, AccountID: 5}
|
|
emailMsg := &EmailMessage{
|
|
MessageID: "msg-123",
|
|
FromAddress: "sender@example.com",
|
|
FromName: "Sender Name",
|
|
ToAddresses: []string{"support@gochat.com"},
|
|
CcAddresses: []string{"cc@example.com"},
|
|
Subject: "Test Subject",
|
|
TextContent: "Hello world",
|
|
InReplyTo: "parent-msg",
|
|
References: "<parent-msg> <child-msg>",
|
|
Attachments: []EmailAttachment{
|
|
{Filename: "file.pdf", ContentType: "application/pdf", FileSize: 1024},
|
|
},
|
|
}
|
|
|
|
msg, err := convertToIncomingMessage(emailMsg, inbox)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, msg)
|
|
|
|
assert.Equal(t, channelpkg.ChannelEmail, msg.ChannelType)
|
|
assert.Equal(t, "msg-123", msg.SourceID)
|
|
assert.Equal(t, "parent-msg", msg.ConversationID)
|
|
assert.Equal(t, "sender@example.com", msg.SenderID)
|
|
assert.Equal(t, "Sender Name", msg.SenderName)
|
|
assert.Equal(t, "Hello world", msg.Content)
|
|
assert.Equal(t, channelpkg.ContentEmail, msg.ContentType)
|
|
assert.Equal(t, uint(10), msg.InboxID)
|
|
assert.Equal(t, uint(5), msg.AccountID)
|
|
assert.Equal(t, "parent-msg", msg.ReplyToID)
|
|
assert.Len(t, msg.Attachments, 1)
|
|
assert.Equal(t, "file.pdf", msg.Attachments[0].Filename)
|
|
}
|
|
|
|
func TestConvertToIncomingMessage_ReplyTo(t *testing.T) {
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
emailMsg := &EmailMessage{
|
|
MessageID: "msg-456",
|
|
FromAddress: "from@example.com",
|
|
ReplyToAddress: "reply@example.com",
|
|
TextContent: "body",
|
|
}
|
|
msg, err := convertToIncomingMessage(emailMsg, inbox)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "reply@example.com", msg.SenderID)
|
|
}
|
|
|
|
func TestConvertToIncomingMessage_HTMLFallback(t *testing.T) {
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
emailMsg := &EmailMessage{
|
|
MessageID: "msg-789",
|
|
FromAddress: "from@example.com",
|
|
HTMLContent: "<p>HTML body</p>",
|
|
}
|
|
msg, err := convertToIncomingMessage(emailMsg, inbox)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "HTML body", msg.Content)
|
|
}
|
|
|
|
func TestConvertToIncomingMessage_EmptyContent(t *testing.T) {
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
emailMsg := &EmailMessage{
|
|
MessageID: "msg-empty",
|
|
FromAddress: "from@example.com",
|
|
}
|
|
msg, err := convertToIncomingMessage(emailMsg, inbox)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "(no content)", msg.Content)
|
|
}
|
|
|
|
// ============================================================
|
|
// Provider tests
|
|
// ============================================================
|
|
|
|
func TestNewEmailProvider(t *testing.T) {
|
|
svc := NewService()
|
|
repo := &Repository{}
|
|
pipe := NewIncomingProcessor(nil)
|
|
provider := NewEmailProvider(svc, repo, pipe)
|
|
assert.NotNil(t, provider)
|
|
}
|
|
|
|
func TestEmailProvider_Identity(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
assert.Equal(t, channelpkg.ChannelEmail, provider.Type())
|
|
assert.Equal(t, "Email", provider.Name())
|
|
assert.NotEmpty(t, provider.Description())
|
|
}
|
|
|
|
func TestEmailProvider_ConfigSchema(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
schema := provider.ConfigSchema()
|
|
require.NotNil(t, schema)
|
|
assert.Equal(t, "object", schema.Type)
|
|
assert.NotEmpty(t, schema.Properties)
|
|
assert.NotEmpty(t, schema.Required)
|
|
}
|
|
|
|
func TestEmailProvider_ValidateConfig(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
ctx := context.Background()
|
|
|
|
t.Run("empty config fails", func(t *testing.T) {
|
|
err := provider.ValidateConfig(ctx, channelpkg.ChannelConfig{})
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("missing imap fields", func(t *testing.T) {
|
|
config := channelpkg.ChannelConfig{
|
|
"imap_address": "imap.example.com",
|
|
}
|
|
err := provider.ValidateConfig(ctx, config)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "imap_port")
|
|
})
|
|
|
|
t.Run("missing smtp fields", func(t *testing.T) {
|
|
config := channelpkg.ChannelConfig{
|
|
"imap_address": "imap.example.com",
|
|
"imap_port": 993,
|
|
"imap_email": "user@example.com",
|
|
"imap_password": "pass",
|
|
"smtp_address": "smtp.example.com",
|
|
}
|
|
err := provider.ValidateConfig(ctx, config)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "smtp_port")
|
|
})
|
|
|
|
t.Run("complete config passes", func(t *testing.T) {
|
|
config := channelpkg.ChannelConfig{
|
|
"imap_address": "imap.example.com",
|
|
"imap_port": 993,
|
|
"imap_email": "user@example.com",
|
|
"imap_password": "pass",
|
|
"smtp_address": "smtp.example.com",
|
|
"smtp_port": 587,
|
|
"smtp_email": "user@example.com",
|
|
"smtp_password": "pass",
|
|
}
|
|
err := provider.ValidateConfig(ctx, config)
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestEmailProvider_DefaultConfig(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
config := provider.DefaultConfig()
|
|
assert.NotNil(t, config)
|
|
assert.Equal(t, 993, config["imap_port"])
|
|
assert.Equal(t, 587, config["smtp_port"])
|
|
assert.Equal(t, true, config["imap_ssl"])
|
|
assert.Equal(t, true, config["smtp_ssl"])
|
|
}
|
|
|
|
func TestEmailProvider_OnCreate(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
ctx := context.Background()
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, AccountID: 1}
|
|
|
|
// OnCreate will try to validate IMAP/SMTP which will fail — expect error
|
|
config := channelpkg.ChannelConfig{
|
|
"smtp_email": "support@example.com",
|
|
}
|
|
_, err := provider.OnCreate(ctx, inbox, config)
|
|
assert.Error(t, err) // IMAP connection validation fails
|
|
}
|
|
|
|
func TestEmailProvider_OnDestroy(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
err := provider.OnDestroy(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, channelpkg.ChannelConfig{})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestEmailProvider_ProcessIncoming(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
ctx := context.Background()
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, AccountID: 1}
|
|
|
|
t.Run("valid payload", func(t *testing.T) {
|
|
emailMsg := EmailMessage{
|
|
MessageID: "test-msg-1",
|
|
FromAddress: "sender@example.com",
|
|
FromName: "Sender",
|
|
Subject: "Test",
|
|
TextContent: "Hello",
|
|
}
|
|
payload, _ := json.Marshal(emailMsg)
|
|
msg, err := provider.ProcessIncoming(ctx, inbox, payload)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, msg)
|
|
assert.Equal(t, "test-msg-1", msg.SourceID)
|
|
assert.Equal(t, "sender@example.com", msg.SenderID)
|
|
assert.Equal(t, "Hello", msg.Content)
|
|
assert.Equal(t, channelpkg.ChannelEmail, msg.ChannelType)
|
|
})
|
|
|
|
t.Run("invalid JSON", func(t *testing.T) {
|
|
_, err := provider.ProcessIncoming(ctx, inbox, []byte("invalid json"))
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("with attachments", func(t *testing.T) {
|
|
emailMsg := EmailMessage{
|
|
MessageID: "test-msg-2",
|
|
FromAddress: "sender@example.com",
|
|
TextContent: "With attachment",
|
|
Attachments: []EmailAttachment{
|
|
{Filename: "doc.pdf", ContentType: "application/pdf"},
|
|
},
|
|
}
|
|
payload, _ := json.Marshal(emailMsg)
|
|
msg, err := provider.ProcessIncoming(ctx, inbox, payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, msg.Attachments, 1)
|
|
assert.Equal(t, "doc.pdf", msg.Attachments[0].Filename)
|
|
})
|
|
|
|
t.Run("with reply-to", func(t *testing.T) {
|
|
emailMsg := EmailMessage{
|
|
MessageID: "test-msg-3",
|
|
FromAddress: "sender@example.com",
|
|
TextContent: "Reply",
|
|
InReplyTo: "original-msg",
|
|
}
|
|
payload, _ := json.Marshal(emailMsg)
|
|
msg, err := provider.ProcessIncoming(ctx, inbox, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "original-msg", msg.ReplyToID)
|
|
})
|
|
}
|
|
|
|
func TestEmailProvider_ValidateWebhookRequest(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
err := provider.ValidateWebhookRequest(context.Background(), &model.Inbox{}, &channelpkg.WebhookRequest{})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestEmailProvider_SendMessage(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
ctx := context.Background()
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, AccountID: 1}
|
|
|
|
t.Run("no contact source_id", func(t *testing.T) {
|
|
contact := &model.Contact{}
|
|
msg := &model.Message{Content: "test"}
|
|
_, err := provider.SendMessage(ctx, inbox, msg, contact)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("no smtp_email config", func(t *testing.T) {
|
|
contact := &model.Contact{SourceID: "recipient@example.com"}
|
|
msg := &model.Message{Content: "test"}
|
|
_, err := provider.SendMessage(ctx, inbox, msg, contact)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "smtp_email")
|
|
})
|
|
}
|
|
|
|
func TestEmailProvider_GetContactProfile(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
profile, err := provider.GetContactProfile(context.Background(), &model.Inbox{}, "user@example.com")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "user@example.com", profile.Name)
|
|
assert.Equal(t, "user@example.com", profile.Extra["email"])
|
|
}
|
|
|
|
func TestEmailProvider_Capabilities(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
caps := provider.Capabilities()
|
|
assert.True(t, caps.SupportsAttachments)
|
|
assert.True(t, caps.SupportsDeliveryStatus)
|
|
assert.True(t, caps.SupportsReplies)
|
|
assert.True(t, caps.SupportsEmailHeaders)
|
|
assert.False(t, caps.SupportsTypingIndicator)
|
|
assert.Equal(t, int64(25*1024*1024), caps.MaxAttachmentSize)
|
|
}
|
|
|
|
func TestEmailProvider_PollInterval(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
assert.Equal(t, 5*time.Minute, provider.PollInterval())
|
|
}
|
|
|
|
func TestEmailProvider_PollMessages_EmptyConfig(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
msgs, err := provider.PollMessages(context.Background(), inbox)
|
|
// IMAP fetch will fail due to incomplete config
|
|
assert.Error(t, err)
|
|
assert.Nil(t, msgs)
|
|
}
|
|
|
|
func TestEmailProvider_SetPipeline(t *testing.T) {
|
|
provider := NewEmailProvider(NewService(), &Repository{}, NewIncomingProcessor(nil))
|
|
newPipe := NewIncomingProcessor(nil)
|
|
provider.SetPipeline(newPipe)
|
|
assert.NotNil(t, provider.pipeline)
|
|
}
|
|
|
|
// ============================================================
|
|
// Helper functions tests
|
|
// ============================================================
|
|
|
|
func TestParseInboxConfig(t *testing.T) {
|
|
t.Run("empty string", func(t *testing.T) {
|
|
config := parseInboxConfig("")
|
|
assert.NotNil(t, config)
|
|
assert.Empty(t, config)
|
|
})
|
|
|
|
t.Run("valid JSON", func(t *testing.T) {
|
|
config := parseInboxConfig(`{"key":"value"}`)
|
|
assert.Equal(t, "value", config["key"])
|
|
})
|
|
|
|
t.Run("invalid JSON", func(t *testing.T) {
|
|
config := parseInboxConfig("invalid")
|
|
assert.NotNil(t, config)
|
|
assert.Empty(t, config)
|
|
})
|
|
}
|
|
|
|
func TestConfigStr(t *testing.T) {
|
|
assert.Equal(t, "default", configStr(channelpkg.ChannelConfig{}, "key", "default"))
|
|
assert.Equal(t, "value", configStr(channelpkg.ChannelConfig{"key": "value"}, "key", "default"))
|
|
assert.Equal(t, "default", configStr(channelpkg.ChannelConfig{"key": ""}, "key", "default"))
|
|
assert.Equal(t, "default", configStr(channelpkg.ChannelConfig{"key": 123}, "key", "default"))
|
|
}
|
|
|
|
// ============================================================
|
|
// IMAPListener tests
|
|
// ============================================================
|
|
|
|
func TestNewIMAPListener(t *testing.T) {
|
|
l := NewIMAPListener()
|
|
assert.NotNil(t, l)
|
|
assert.NotNil(t, l.lastUID)
|
|
}
|
|
|
|
func TestIMAPListener_ValidateConnection_IncompleteConfig(t *testing.T) {
|
|
l := NewIMAPListener()
|
|
err := l.ValidateConnection(channelpkg.ChannelConfig{})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "required")
|
|
}
|
|
|
|
func TestIMAPListener_Fetch_IncompleteConfig(t *testing.T) {
|
|
l := NewIMAPListener()
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
_, err := l.Fetch(context.Background(), inbox, channelpkg.ChannelConfig{})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "incomplete")
|
|
}
|
|
|
|
// ============================================================
|
|
// SMTPSender tests
|
|
// ============================================================
|
|
|
|
func TestNewSMTPSender(t *testing.T) {
|
|
s := NewSMTPSender()
|
|
assert.NotNil(t, s)
|
|
}
|
|
|
|
func TestSMTPSender_ValidateConnection_IncompleteConfig(t *testing.T) {
|
|
s := NewSMTPSender()
|
|
err := s.ValidateConnection(channelpkg.ChannelConfig{})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "required")
|
|
}
|
|
|
|
func TestSMTPSender_Send_IncompleteConfig(t *testing.T) {
|
|
s := NewSMTPSender()
|
|
_, err := s.Send(channelpkg.ChannelConfig{}, "", "to@test.com", "subject", "body")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "incomplete")
|
|
}
|
|
|
|
func TestComposeSMTPMessage_PlainText(t *testing.T) {
|
|
msg := composeSMTPMessage("from@test.com", "to@test.com", "Subject", "Hello body", "<msg-id@test>")
|
|
assert.Contains(t, msg, "From: from@test.com")
|
|
assert.Contains(t, msg, "To: to@test.com")
|
|
assert.Contains(t, msg, "Subject: Subject")
|
|
assert.Contains(t, msg, "Message-Id: <msg-id@test>")
|
|
assert.Contains(t, msg, "Content-Type: text/plain")
|
|
assert.Contains(t, msg, "Hello body")
|
|
}
|
|
|
|
func TestComposeSMTPMessage_HTML(t *testing.T) {
|
|
msg := composeSMTPMessage("from@test.com", "to@test.com", "Subject", "<p>HTML body</p>", "<msg-id@test>")
|
|
assert.Contains(t, msg, "Content-Type: text/html")
|
|
}
|
|
|
|
func TestExtractMessageIDFromRaw(t *testing.T) {
|
|
raw := []byte("From: a@b\r\nMessage-Id: <test@id>\r\n\r\nbody")
|
|
id := extractMessageIDFromRaw(raw)
|
|
assert.Equal(t, "<test@id>", id)
|
|
|
|
raw2 := []byte("From: a@b\r\nMessage-ID: <test2@id>\r\n\r\nbody")
|
|
id2 := extractMessageIDFromRaw(raw2)
|
|
assert.Equal(t, "<test2@id>", id2)
|
|
|
|
raw3 := []byte("no message id here")
|
|
id3 := extractMessageIDFromRaw(raw3)
|
|
assert.Equal(t, "", id3)
|
|
}
|
|
|
|
// ============================================================
|
|
// Pipeline tests
|
|
// ============================================================
|
|
|
|
func TestNewIncomingProcessor(t *testing.T) {
|
|
p := NewIncomingProcessor(nil)
|
|
assert.NotNil(t, p)
|
|
}
|
|
|
|
func TestIncomingProcessor_Process_NilHandler(t *testing.T) {
|
|
p := NewIncomingProcessor(nil)
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
emailMsg := &EmailMessage{MessageID: "test"}
|
|
_, err := p.Process(context.Background(), inbox, emailMsg)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not initialized")
|
|
}
|
|
|
|
func TestIncomingProcessor_Process_WithHandler(t *testing.T) {
|
|
// Use a mock handler that returns a dummy IncomingMessage
|
|
mockHandler := &mockIncomingHandler{}
|
|
p := NewIncomingProcessor(mockHandler)
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
emailMsg := &EmailMessage{MessageID: "test", FromAddress: "a@b.com"}
|
|
_, err := p.Process(context.Background(), inbox, emailMsg)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestNewOutgoingProcessor(t *testing.T) {
|
|
p := NewOutgoingProcessor(nil)
|
|
assert.NotNil(t, p)
|
|
}
|
|
|
|
func TestOutgoingProcessor_Process_NilHandler(t *testing.T) {
|
|
p := NewOutgoingProcessor(nil)
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
msg := &model.Message{Content: "test"}
|
|
contact := &model.Contact{}
|
|
_, err := p.Process(context.Background(), inbox, msg, contact)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not initialized")
|
|
}
|
|
|
|
func TestSerializeEmailMessage(t *testing.T) {
|
|
emailMsg := &EmailMessage{
|
|
MessageID: "test-id",
|
|
FromAddress: "from@test.com",
|
|
Subject: "Test",
|
|
TextContent: "body",
|
|
}
|
|
data, err := serializeEmailMessage(emailMsg)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, data)
|
|
|
|
// Verify it's valid JSON
|
|
var m map[string]interface{}
|
|
err = json.Unmarshal(data, &m)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test-id", m["message_id"])
|
|
}
|
|
|
|
// ============================================================
|
|
// Repository tests (using in-memory sqlite)
|
|
// ============================================================
|
|
|
|
func TestRepository_PollStateTableName(t *testing.T) {
|
|
assert.Equal(t, "email_poll_states", PollState{}.TableName())
|
|
}
|
|
|
|
// ============================================================
|
|
// Webhook handler tests
|
|
// ============================================================
|
|
|
|
func TestNewWebhookHandler(t *testing.T) {
|
|
h := NewWebhookHandler()
|
|
assert.NotNil(t, h)
|
|
}
|
|
|
|
func TestWebhookHandler_HandleWebhookRequest_EmptyBody(t *testing.T) {
|
|
h := NewWebhookHandler()
|
|
req := httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader(""))
|
|
w := httptest.NewRecorder()
|
|
h.HandleWebhookRequest(w, req)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestWebhookHandler_HandleWebhookRequest_GenericJSON(t *testing.T) {
|
|
h := NewWebhookHandler()
|
|
body := `{"from":"sender@example.com","subject":"Test","text":"Hello"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
h.HandleWebhookRequest(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
func TestWebhookHandler_HandleWebhookRequest_MailgunFormat(t *testing.T) {
|
|
h := NewWebhookHandler()
|
|
body := `{"from":"sender@example.com","subject":"MG Test","body-plain":"Hello MG","Message-Id":"<mg123@example.com>"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader(body))
|
|
req.Header.Set("X-Mailgun-Sid", "mg-123")
|
|
w := httptest.NewRecorder()
|
|
h.HandleWebhookRequest(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
func TestWebhookHandler_HandleWebhookRequest_SendGridFormat(t *testing.T) {
|
|
h := NewWebhookHandler()
|
|
body := `{"envelope":{"from":"sg@example.com","to":["support@gochat.com"]},"subject":"SG Test","text":"Hello SG","messageId":"sg-123"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader(body))
|
|
req.Header.Set("X-Sg-Envelope-Id", "sg-456")
|
|
w := httptest.NewRecorder()
|
|
h.HandleWebhookRequest(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
func TestWebhookHandler_HandleWebhookRequest_RawEmail(t *testing.T) {
|
|
h := NewWebhookHandler()
|
|
body := "From: raw@example.com\r\nSubject: Raw Test\r\n\r\nHello raw email"
|
|
req := httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.HandleWebhookRequest(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
func TestWebhookHandler_ParseWebhookBody_GenericJSON(t *testing.T) {
|
|
h := NewWebhookHandler()
|
|
body := []byte(`{"from":"sender@example.com","subject":"Test","text":"Hello","to":"support@gochat.com"}`)
|
|
headers := http.Header{}
|
|
headers.Set("Content-Type", "application/json")
|
|
msg, err := h.ParseWebhookBody(body, headers)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "sender@example.com", msg.FromAddress)
|
|
assert.Equal(t, "Test", msg.Subject)
|
|
assert.Equal(t, "Hello", msg.TextContent)
|
|
assert.Equal(t, []string{"support@gochat.com"}, msg.ToAddresses)
|
|
}
|
|
|
|
func TestWebhookHandler_ParseWebhookBody_Mailgun(t *testing.T) {
|
|
h := NewWebhookHandler()
|
|
body := []byte(`{"from":"mg@example.com","subject":"MG","body-plain":"text","body-html":"<p>html</p>","Message-Id":"<mg1@ex.com>","to":"support@gochat.com"}`)
|
|
headers := http.Header{}
|
|
headers.Set("X-Mailgun-Sid", "mg-123")
|
|
msg, err := h.ParseWebhookBody(body, headers)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "mg@example.com", msg.FromAddress)
|
|
assert.Equal(t, "MG", msg.Subject)
|
|
assert.Equal(t, "text", msg.TextContent)
|
|
assert.Equal(t, "<p>html</p>", msg.HTMLContent)
|
|
assert.Equal(t, "<mg1@ex.com>", msg.MessageID)
|
|
}
|
|
|
|
func TestWebhookHandler_ParseWebhookBody_SendGrid(t *testing.T) {
|
|
h := NewWebhookHandler()
|
|
body := []byte(`{"envelope":{"from":"sg@example.com","to":["support@gochat.com"]},"subject":"SG","text":"SG text","html":"<p>SG html</p>","messageId":"sg-123"}`)
|
|
headers := http.Header{}
|
|
headers.Set("X-Sg-Envelope-Id", "sg-456")
|
|
msg, err := h.ParseWebhookBody(body, headers)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "sg@example.com", msg.FromAddress)
|
|
assert.Equal(t, "SG", msg.Subject)
|
|
assert.Equal(t, "SG text", msg.TextContent)
|
|
assert.Equal(t, []string{"support@gochat.com"}, msg.ToAddresses)
|
|
assert.Equal(t, "sg-123", msg.MessageID)
|
|
}
|
|
|
|
func TestWebhookHandler_ParseWebhookBody_RawEmail(t *testing.T) {
|
|
h := NewWebhookHandler()
|
|
body := []byte("This is a raw email body")
|
|
headers := http.Header{}
|
|
msg, err := h.ParseWebhookBody(body, headers)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "This is a raw email body", msg.TextContent)
|
|
}
|
|
|
|
func TestIsMailgunPayload(t *testing.T) {
|
|
headers := http.Header{}
|
|
headers.Set("X-Mailgun-Sid", "123")
|
|
assert.True(t, isMailgunPayload(headers))
|
|
|
|
headers2 := http.Header{}
|
|
assert.False(t, isMailgunPayload(headers2))
|
|
}
|
|
|
|
func TestIsSendGridPayload(t *testing.T) {
|
|
headers := http.Header{}
|
|
headers.Set("X-Sg-Envelope-Id", "456")
|
|
assert.True(t, isSendGridPayload(headers))
|
|
|
|
headers2 := http.Header{}
|
|
assert.False(t, isSendGridPayload(headers2))
|
|
}
|
|
|
|
// ============================================================
|
|
// Mock handlers
|
|
// ============================================================
|
|
|
|
type mockIncomingHandler struct{}
|
|
|
|
func (m *mockIncomingHandler) ProcessIncoming(ctx context.Context, inbox *model.Inbox, rawPayload []byte) (*channelpkg.IncomingMessage, error) {
|
|
return &channelpkg.IncomingMessage{
|
|
ChannelType: channelpkg.ChannelEmail,
|
|
SourceID: "mock-msg-id",
|
|
InboxID: inbox.ID,
|
|
}, nil
|
|
}
|