package email import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" channelpkg "github.com/gochat/gochat/internal/channel" "github.com/gochat/gochat/internal/model" ) // safeCallCov10 runs fn and recovers from panics (nil-dep panics etc). func safeCallCov10(fn func()) { defer func() { _ = recover() }() fn() } // ============================================================ // provider.go — ValidateConfig edge cases // ============================================================ func TestValidateConfig_FloatPort_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) err := p.ValidateConfig(context.Background(), channelpkg.ChannelConfig{ "imap_address": "imap.test.com", "imap_port": float64(993), "imap_email": "user@test.com", "imap_password": "pass", "smtp_address": "smtp.test.com", "smtp_port": float64(587), "smtp_email": "user@test.com", "smtp_password": "pass", }) require.NoError(t, err) } func TestValidateConfig_Int64Port_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) err := p.ValidateConfig(context.Background(), channelpkg.ChannelConfig{ "imap_address": "imap.test.com", "imap_port": int64(993), "imap_email": "user@test.com", "imap_password": "pass", "smtp_address": "smtp.test.com", "smtp_port": int64(587), "smtp_email": "user@test.com", "smtp_password": "pass", }) require.NoError(t, err) } func TestValidateConfig_StringPort_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) err := p.ValidateConfig(context.Background(), channelpkg.ChannelConfig{ "imap_address": "imap.test.com", "imap_port": "993", "imap_email": "user@test.com", "imap_password": "pass", "smtp_address": "smtp.test.com", "smtp_port": "587", "smtp_email": "user@test.com", "smtp_password": "pass", }) require.NoError(t, err) } func TestValidateConfig_InvalidStringPort_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) err := p.ValidateConfig(context.Background(), channelpkg.ChannelConfig{ "imap_address": "imap.test.com", "imap_port": "abc", "imap_email": "user@test.com", "imap_password": "pass", "smtp_address": "smtp.test.com", "smtp_port": "587", "smtp_email": "user@test.com", "smtp_password": "pass", }) require.Error(t, err) assert.Contains(t, err.Error(), "imap_port") } func TestValidateConfig_BoolPort_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) err := p.ValidateConfig(context.Background(), channelpkg.ChannelConfig{ "imap_address": "imap.test.com", "imap_port": true, "imap_email": "user@test.com", "imap_password": "pass", "smtp_address": "smtp.test.com", "smtp_port": 587, "smtp_email": "user@test.com", "smtp_password": "pass", }) require.Error(t, err) assert.Contains(t, err.Error(), "imap_port") } func TestValidateConfig_DefaultPort_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) // Missing imap_port entirely → default 0 → error err := p.ValidateConfig(context.Background(), channelpkg.ChannelConfig{ "imap_address": "imap.test.com", "imap_email": "user@test.com", "imap_password": "pass", "smtp_address": "smtp.test.com", "smtp_port": 587, "smtp_email": "user@test.com", "smtp_password": "pass", }) require.Error(t, err) assert.Contains(t, err.Error(), "imap_port") } // ============================================================ // provider.go — OnCreate forward_to_address path // ============================================================ func TestOnCreate_ForwardToAddressSet_Cov10(t *testing.T) { // OnCreate with a Service that has nil imap/smtp → will panic on nil dep. // Use safeCall to handle that. p := NewEmailProvider(&Service{}, nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}} safeCallCov10(func() { cfg, err := p.OnCreate(context.Background(), inbox, channelpkg.ChannelConfig{ "imap_address": "imap.test.com", "imap_port": 993, "imap_email": "user@test.com", "imap_password": "pass", "smtp_address": "smtp.test.com", "smtp_port": 587, "smtp_email": "user@test.com", "smtp_password": "pass", }) // Will error on IMAP validation, but the smtp_email is set in config _ = cfg _ = err }) } func TestOnCreate_NilService_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}} safeCallCov10(func() { _, _ = p.OnCreate(context.Background(), inbox, channelpkg.ChannelConfig{}) }) } func TestOnCreate_SMTPFail_Cov10(t *testing.T) { // Use a Service with nil imapListener → IMAP validation panics p := &EmailProvider{ service: &Service{}, } inbox := &model.Inbox{Base: model.Base{ID: 1}} safeCallCov10(func() { _, _ = p.OnCreate(context.Background(), inbox, channelpkg.ChannelConfig{ "imap_address": "127.0.0.1", "imap_port": 1, "imap_email": "user@test.com", "imap_password": "pass", "smtp_address": "smtp.test.com", "smtp_port": 587, "smtp_email": "user@test.com", "smtp_password": "pass", }) }) } // ============================================================ // provider.go — ProcessIncoming with various EmailMessage fields // ============================================================ func TestProcessIncoming_WithHTMLContent_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}, AccountID: 10} emailMsg := EmailMessage{ MessageID: "msg-html", FromAddress: "sender@test.com", FromName: "Sender", Subject: "HTML Test", HTMLContent: "
Hello
", TextContent: "Hello", ToAddresses: []string{"to@test.com"}, CcAddresses: []string{"cc@test.com"}, ReplyToAddress: "reply@test.com", References: "Hello
", msg.Extra["html_content"]) assert.Equal(t, "reply@test.com", msg.Extra["reply_to_address"]) } func TestProcessIncoming_WithUID_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}, AccountID: 10} emailMsg := EmailMessage{ MessageID: "msg-uid", UID: 42, } payload, _ := json.Marshal(emailMsg) msg, err := p.ProcessIncoming(context.Background(), inbox, payload) require.NoError(t, err) require.NotNil(t, msg) // UID should be in Extra uidVal, ok := msg.Extra["imap_uid"] assert.True(t, ok) _ = uidVal } func TestProcessIncoming_NoInReplyTo_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}, AccountID: 10} emailMsg := EmailMessage{ MessageID: "msg-no-reply", FromAddress: "sender@test.com", TextContent: "hello", } payload, _ := json.Marshal(emailMsg) msg, err := p.ProcessIncoming(context.Background(), inbox, payload) require.NoError(t, err) require.NotNil(t, msg) assert.Empty(t, msg.ReplyToID) } func TestProcessIncoming_WithInReplyTo_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}, AccountID: 10} emailMsg := EmailMessage{ MessageID: "msg-reply", InReplyTo: "parent-msg-id", } payload, _ := json.Marshal(emailMsg) msg, err := p.ProcessIncoming(context.Background(), inbox, payload) require.NoError(t, err) require.NotNil(t, msg) assert.Equal(t, "parent-msg-id", msg.ReplyToID) assert.Equal(t, "parent-msg-id", msg.ConversationID) } func TestProcessIncoming_EmptyPayload_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}} _, err := p.ProcessIncoming(context.Background(), inbox, []byte{}) require.Error(t, err) } func TestProcessIncoming_NilPayload_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}} _, err := p.ProcessIncoming(context.Background(), inbox, nil) require.Error(t, err) } func TestProcessIncoming_MultipleAttachments_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}, AccountID: 10} emailMsg := EmailMessage{ MessageID: "msg-multi-att", FromAddress: "sender@test.com", TextContent: "see attachments", Attachments: []EmailAttachment{ {Filename: "file1.pdf", ContentType: "application/pdf", URL: "https://example.com/f1", FileSize: 1024}, {Filename: "file2.png", ContentType: "image/png", URL: "https://example.com/f2", FileSize: 2048}, {Filename: "file3.txt", ContentType: "text/plain", URL: "https://example.com/f3"}, }, } payload, _ := json.Marshal(emailMsg) msg, err := p.ProcessIncoming(context.Background(), inbox, payload) require.NoError(t, err) require.NotNil(t, msg) assert.Len(t, msg.Attachments, 3) assert.Equal(t, "file1.pdf", msg.Attachments[0].Filename) assert.Equal(t, int64(1024), msg.Attachments[0].FileSize) assert.Equal(t, "file3.txt", msg.Attachments[2].Filename) } // ============================================================ // provider.go — PollMessages with messages // ============================================================ func TestPollMessages_WithMessages_Cov10(t *testing.T) { // PollMessages with a nil service → will panic, use safeCall p := &EmailProvider{} inbox := &model.Inbox{Base: model.Base{ID: 1}} safeCallCov10(func() { _, _ = p.PollMessages(context.Background(), inbox) }) } func TestPollMessages_WithValidConfig_Cov10(t *testing.T) { p := NewEmailProvider(NewService(), nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}} inbox.ChannelConfig = `{"imap_address":"127.0.0.1","imap_port":1,"imap_login":"user","imap_password":"pass","imap_ssl_mode":"none"}` // Will fail on connection but exercise the config parsing path _, _ = p.PollMessages(context.Background(), inbox) } func TestPollMessages_NilInbox_Cov10(t *testing.T) { p := &EmailProvider{} safeCallCov10(func() { _, _ = p.PollMessages(context.Background(), nil) }) } // ============================================================ // provider.go — SendMessage edge cases // ============================================================ func TestSendMessage_EmptyContact_Cov10(t *testing.T) { p := NewEmailProvider(NewService(), nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}} inbox.ChannelConfig = `{"smtp_email":"from@test.com"}` contact := &model.Contact{} _, err := p.SendMessage(context.Background(), inbox, &model.Message{Content: "test"}, contact) require.Error(t, err) assert.Contains(t, err.Error(), "source_id") } func TestSendMessage_NilPipeline_Cov10(t *testing.T) { p := NewEmailProvider(NewService(), nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}} inbox.ChannelConfig = `{"smtp_email":"from@test.com"}` contact := &model.Contact{SourceID: "to@test.com"} // This will attempt SMTP send and fail with network error _, err := p.SendMessage(context.Background(), inbox, &model.Message{Content: "test"}, contact) require.Error(t, err) } func TestSendMessage_SuccessResult_Cov10(t *testing.T) { // Test the result structure when SMTP succeeds (unlikely without a server) // But we can verify the error path p := NewEmailProvider(NewService(), nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 5}} inbox.ChannelConfig = `{"smtp_email":"from@test.com","smtp_address":"127.0.0.1","smtp_port":1,"smtp_login":"user","smtp_password":"pass","smtp_ssl_mode":"none"}` contact := &model.Contact{SourceID: "to@test.com"} msg := &model.Message{Content: "hello world"} _, err := p.SendMessage(context.Background(), inbox, msg, contact) require.Error(t, err) assert.Contains(t, err.Error(), "SMTP") } // ============================================================ // provider.go — GetContactProfile // ============================================================ func TestGetContactProfile_EmptySource_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{} profile, err := p.GetContactProfile(context.Background(), inbox, "") require.NoError(t, err) require.NotNil(t, profile) assert.Equal(t, "", profile.Name) assert.Equal(t, "", profile.Extra["email"]) } func TestGetContactProfile_WithDomain_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{} profile, err := p.GetContactProfile(context.Background(), inbox, "user@example.com") require.NoError(t, err) require.NotNil(t, profile) assert.Equal(t, "user@example.com", profile.Name) assert.Equal(t, "user@example.com", profile.Extra["email"]) } // ============================================================ // provider.go — ConfigSchema properties // ============================================================ func TestConfigSchema_AllProperties_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) schema := p.ConfigSchema() require.NotNil(t, schema) expectedProps := []string{ "imap_address", "imap_port", "imap_email", "imap_password", "imap_ssl", "smtp_address", "smtp_port", "smtp_email", "smtp_password", "smtp_ssl", } for _, prop := range expectedProps { assert.Contains(t, schema.Properties, prop, "missing property: %s", prop) } // Check imap_port default assert.Equal(t, 993, schema.Properties["imap_port"].Default) assert.Equal(t, 587, schema.Properties["smtp_port"].Default) // Check required fields for _, req := range expectedProps { if req == "imap_ssl" || req == "smtp_ssl" { continue // These are not required } assert.Contains(t, schema.Required, req, "missing required: %s", req) } // Check secret fields assert.True(t, schema.Properties["imap_password"].Secret) assert.True(t, schema.Properties["smtp_password"].Secret) // Check email format assert.Equal(t, "email", schema.Properties["imap_email"].Format) assert.Equal(t, "email", schema.Properties["smtp_email"].Format) } func TestDefaultConfig_Values_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) cfg := p.DefaultConfig() assert.Equal(t, 993, cfg["imap_port"]) assert.Equal(t, 587, cfg["smtp_port"]) assert.True(t, cfg["imap_ssl"].(bool)) assert.True(t, cfg["smtp_ssl"].(bool)) } // ============================================================ // provider.go — Capabilities // ============================================================ func TestCapabilities_FullCheck_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) caps := p.Capabilities() assert.True(t, caps.SupportsAttachments) assert.False(t, caps.SupportsLocation) assert.False(t, caps.SupportsTypingIndicator) assert.True(t, caps.SupportsDeliveryStatus) assert.True(t, caps.SupportsReplies) assert.False(t, caps.SupportsEmojiReactions) assert.False(t, caps.SupportsVoiceMessages) assert.False(t, caps.SupportsVideoCalls) assert.False(t, caps.SupportsCustomCards) assert.False(t, caps.SupportsTemplates) assert.True(t, caps.SupportsEmailHeaders) assert.Equal(t, int64(25*1024*1024), caps.MaxAttachmentSize) assert.Equal(t, 0, caps.MaxTextLength) } // ============================================================ // smtp_sender.go — Send with various configs // ============================================================ func TestSMTPSend_DefaultSSLMode_Cov10(t *testing.T) { s := NewSMTPSender() _, err := s.Send(channelpkg.ChannelConfig{ "smtp_address": "127.0.0.1", "smtp_port": 1, "smtp_login": "user", "smtp_password": "pass", }, "from@test.com", "to@test.com", "subj", "body") require.Error(t, err) } func TestSMTPSend_EmptyFrom_Cov10(t *testing.T) { s := NewSMTPSender() _, err := s.Send(channelpkg.ChannelConfig{ "smtp_address": "127.0.0.1", "smtp_port": 1, "smtp_login": "user", "smtp_password": "pass", "smtp_ssl_mode": "none", "email": "default@test.com", }, "", "to@test.com", "subj", "body") require.Error(t, err) } func TestSMTPSend_WithMailboxName_Cov10(t *testing.T) { s := NewSMTPSender() _, err := s.Send(channelpkg.ChannelConfig{ "smtp_address": "127.0.0.1", "smtp_port": 1, "smtp_login": "user", "smtp_password": "pass", "smtp_ssl_mode": "none", "mailbox_name": "Support Team", }, "from@test.com", "to@test.com", "subj", "body") require.Error(t, err) } func TestSMTPSend_SSLMode_Cov10(t *testing.T) { s := NewSMTPSender() _, err := s.Send(channelpkg.ChannelConfig{ "smtp_address": "127.0.0.1", "smtp_port": 1, "smtp_login": "user", "smtp_password": "pass", "smtp_ssl_mode": "ssl", }, "from@test.com", "to@test.com", "subj", "body") require.Error(t, err) assert.Contains(t, err.Error(), "TLS") } func TestSMTPSend_STARTTLSMode_Cov10(t *testing.T) { s := NewSMTPSender() _, err := s.Send(channelpkg.ChannelConfig{ "smtp_address": "127.0.0.1", "smtp_port": 1, "smtp_login": "user", "smtp_password": "pass", "smtp_ssl_mode": "starttls", }, "from@test.com", "to@test.com", "subj", "body") require.Error(t, err) } func TestSMTPSend_NoPassword_Cov10(t *testing.T) { s := NewSMTPSender() _, err := s.Send(channelpkg.ChannelConfig{ "smtp_address": "smtp.test.com", "smtp_login": "user", }, "from@test.com", "to@test.com", "subj", "body") // address and login are set but password is missing → "address and login required" only checks those two // So it proceeds to connection attempt require.Error(t, err) } func TestSMTPValidateConnection_DefaultMode_Cov10(t *testing.T) { s := NewSMTPSender() err := s.ValidateConnection(channelpkg.ChannelConfig{ "smtp_address": "127.0.0.1", "smtp_port": 1, "smtp_login": "user", "smtp_password": "pass", }) require.Error(t, err) } func TestSMTPValidateConnection_SSLMode_Cov10(t *testing.T) { s := NewSMTPSender() err := s.ValidateConnection(channelpkg.ChannelConfig{ "smtp_address": "127.0.0.1", "smtp_port": 1, "smtp_login": "user", "smtp_password": "pass", "smtp_ssl_mode": "ssl", }) require.Error(t, err) } func TestSMTPValidateConnection_STARTTLSMode_Cov10(t *testing.T) { s := NewSMTPSender() err := s.ValidateConnection(channelpkg.ChannelConfig{ "smtp_address": "127.0.0.1", "smtp_port": 1, "smtp_login": "user", "smtp_password": "pass", "smtp_ssl_mode": "starttls", }) require.Error(t, err) } // ============================================================ // smtp_sender.go — composeSMTPMessage edge cases // ============================================================ func TestComposeSMTPMessage_EmptyBody_Cov10(t *testing.T) { msg := composeSMTPMessage("from@test.com", "to@test.com", "Test", "", "Hello
" msg := composeSMTPMessage("from@test.com", "to@test.com", "HTML Test", body, "Paragraph
" msg := composeSMTPMessage("from@test.com", "to@test.com", "P Test", body, "Hello
"}` headers := http.Header{} headers.Set("X-Sg-Envelope-Id", "env-123") msg, err := h.ParseWebhookBody([]byte(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, "Hello
", msg.HTMLContent) assert.Equal(t, []string{"recipient@example.com"}, msg.ToAddresses) } func TestParseWebhookBody_GenericJSON_Cov10(t *testing.T) { h := NewWebhookHandler() body := `{"from":"sender@example.com","subject":"Test","text":"Hello","to":"recipient@example.com"}` headers := http.Header{} headers.Set("Content-Type", "application/json") msg, err := h.ParseWebhookBody([]byte(body), headers) require.NoError(t, err) assert.Equal(t, "sender@example.com", msg.FromAddress) assert.Equal(t, "Test", msg.Subject) assert.Equal(t, []string{"recipient@example.com"}, msg.ToAddresses) } func TestParseWebhookBody_RawEmail_Cov10(t *testing.T) { h := NewWebhookHandler() body := "From: sender@example.com\r\nSubject: Test\r\n\r\nHello world" msg, err := h.ParseWebhookBody([]byte(body), http.Header{}) require.NoError(t, err) assert.NotEmpty(t, msg.TextContent) assert.Contains(t, msg.TextContent, "Hello world") } func TestParseWebhookBody_MailgunWithAllFields_Cov10(t *testing.T) { h := NewWebhookHandler() body := `{"from":"sender@example.com","subject":"Re: Test","body-plain":"Hello","body-html":"Hello
","to":"recipient@example.com","Message-Id":"msg-full","In-Reply-To":"parent","References":"Hello
", msg.HTMLContent) assert.Equal(t, "msg-full", msg.MessageID) assert.Equal(t, "parent", msg.InReplyTo) assert.Equal(t, "Hello
","messageId":"msg-sg","in_reply_to":"parent","date":"2024-01-01"}` headers := http.Header{} headers.Set("X-Sg-Envelope-Id", "env-123") msg, err := h.ParseWebhookBody([]byte(body), headers) require.NoError(t, err) assert.Equal(t, "sender@example.com", msg.FromAddress) assert.Len(t, msg.ToAddresses, 2) assert.Equal(t, "r1@example.com", msg.ToAddresses[0]) assert.Equal(t, "r2@example.com", msg.ToAddresses[1]) assert.Equal(t, "msg-sg", msg.MessageID) assert.Equal(t, "parent", msg.InReplyTo) assert.Equal(t, "2024-01-01", msg.Date) } func TestParseWebhookBody_SendGridNoEnvelope_Cov10(t *testing.T) { h := NewWebhookHandler() body := `{"subject":"Test","text":"Hello"}` headers := http.Header{} headers.Set("X-Sg-Envelope-Id", "env-123") msg, err := h.ParseWebhookBody([]byte(body), headers) require.NoError(t, err) assert.Equal(t, "Test", msg.Subject) assert.Equal(t, "Hello", msg.TextContent) assert.Empty(t, msg.FromAddress) } func TestParseWebhookBody_SendGridEnvelopeNoTo_Cov10(t *testing.T) { h := NewWebhookHandler() body := `{"envelope":{"from":"sender@example.com"},"subject":"Test"}` headers := http.Header{} headers.Set("X-Sg-Envelope-Id", "env-123") msg, err := h.ParseWebhookBody([]byte(body), headers) require.NoError(t, err) assert.Equal(t, "sender@example.com", msg.FromAddress) assert.Nil(t, msg.ToAddresses) } func TestParseWebhookBody_SendGridBadJSON_Cov10(t *testing.T) { h := NewWebhookHandler() body := "invalid json" headers := http.Header{} headers.Set("X-Sg-Envelope-Id", "env-123") _, err := h.ParseWebhookBody([]byte(body), headers) require.Error(t, err) } func TestParseWebhookBody_MailgunBadJSON_Cov10(t *testing.T) { h := NewWebhookHandler() body := "invalid json" headers := http.Header{} headers.Set("X-Mailgun-Sid", "12345") _, err := h.ParseWebhookBody([]byte(body), headers) require.Error(t, err) } func TestParseWebhookBody_MailgunNoTo_Cov10(t *testing.T) { h := NewWebhookHandler() body := `{"from":"sender@example.com","subject":"Test"}` headers := http.Header{} headers.Set("X-Mailgun-Sid", "12345") msg, err := h.ParseWebhookBody([]byte(body), headers) require.NoError(t, err) assert.Equal(t, "sender@example.com", msg.FromAddress) assert.Nil(t, msg.ToAddresses) } func TestParseWebhookBody_GenericJSONNoTo_Cov10(t *testing.T) { h := NewWebhookHandler() body := `{"from":"sender@example.com","subject":"Test"}` headers := http.Header{} headers.Set("Content-Type", "application/json") msg, err := h.ParseWebhookBody([]byte(body), headers) require.NoError(t, err) assert.Equal(t, "sender@example.com", msg.FromAddress) assert.Nil(t, msg.ToAddresses) } func TestParseWebhookBody_GenericJSONWithHTML_Cov10(t *testing.T) { h := NewWebhookHandler() body := `{"from":"sender@example.com","subject":"Test","html":"HTML
","message_id":"msg-1","in_reply_to":"parent","date":"2024-01-01"}` headers := http.Header{} headers.Set("Content-Type", "application/json") msg, err := h.ParseWebhookBody([]byte(body), headers) require.NoError(t, err) assert.Equal(t, "HTML
", msg.HTMLContent) assert.Equal(t, "msg-1", msg.MessageID) assert.Equal(t, "parent", msg.InReplyTo) assert.Equal(t, "2024-01-01", msg.Date) } // ============================================================ // webhook_handler.go — isMailgunPayload / isSendGridPayload // ============================================================ func TestIsMailgunPayload_True_Cov10(t *testing.T) { headers := http.Header{} headers.Set("X-Mailgun-Sid", "12345") assert.True(t, isMailgunPayload(headers)) } func TestIsMailgunPayload_False_Cov10(t *testing.T) { headers := http.Header{} assert.False(t, isMailgunPayload(headers)) } func TestIsSendGridPayload_True_Cov10(t *testing.T) { headers := http.Header{} headers.Set("X-Sg-Envelope-Id", "env-123") assert.True(t, isSendGridPayload(headers)) } func TestIsSendGridPayload_False_Cov10(t *testing.T) { headers := http.Header{} assert.False(t, isSendGridPayload(headers)) } // ============================================================ // webhook_handler.go — parseRelayPayload direct calls // ============================================================ func TestParseRelayPayload_Mailgun_Cov10(t *testing.T) { body := []byte(`{"from":"sender@example.com","subject":"Test","body-plain":"Hello"}`) headers := http.Header{} headers.Set("X-Mailgun-Sid", "12345") msg, err := parseRelayPayload(body, headers) require.NoError(t, err) assert.Equal(t, "sender@example.com", msg.FromAddress) } func TestParseRelayPayload_SendGrid_Cov10(t *testing.T) { body := []byte(`{"envelope":{"from":"sender@example.com","to":["r@example.com"]},"subject":"Test"}`) headers := http.Header{} headers.Set("X-Sg-Envelope-Id", "env-123") msg, err := parseRelayPayload(body, headers) require.NoError(t, err) assert.Equal(t, "sender@example.com", msg.FromAddress) } func TestParseRelayPayload_GenericJSON_Cov10(t *testing.T) { body := []byte(`{"from":"sender@example.com","subject":"Test","text":"Hello"}`) headers := http.Header{} headers.Set("Content-Type", "application/json") msg, err := parseRelayPayload(body, headers) require.NoError(t, err) assert.Equal(t, "sender@example.com", msg.FromAddress) } func TestParseRelayPayload_RawEmail_Cov10(t *testing.T) { body := []byte("From: sender@example.com\r\nSubject: Test\r\n\r\nHello") msg, err := parseRelayPayload(body, http.Header{}) require.NoError(t, err) assert.NotEmpty(t, msg.TextContent) } func TestParseRelayPayload_InvalidJSONFallsToRaw_Cov10(t *testing.T) { body := []byte("not json at all") headers := http.Header{} headers.Set("Content-Type", "application/json") msg, err := parseRelayPayload(body, headers) // Falls through to raw email parsing require.NoError(t, err) assert.NotEmpty(t, msg.TextContent) } // ============================================================ // service.go — extractEmailAddress / extractDisplayName // ============================================================ func TestExtractEmailAddress_Simple_Cov10(t *testing.T) { assert.Equal(t, "john@example.com", extractEmailAddress("john@example.com")) } func TestExtractEmailAddress_WithName_Cov10(t *testing.T) { assert.Equal(t, "john@example.com", extractEmailAddress("John DoeHello
", Date: "2024-01-01", Attachments: []EmailAttachment{ {Filename: "file.pdf", ContentType: "application/pdf"}, }, } data, err := serializeEmailMessage(emailMsg) require.NoError(t, err) assert.NotEmpty(t, data) var result map[string]interface{} err = json.Unmarshal(data, &result) require.NoError(t, err) assert.Equal(t, "msg1", result["message_id"]) assert.Equal(t, "sender@test.com", result["from_address"]) assert.Equal(t, "Test", result["subject"]) } func TestSerializeEmailMessage_Empty_Cov10(t *testing.T) { data, err := serializeEmailMessage(&EmailMessage{}) require.NoError(t, err) assert.NotEmpty(t, data) } // ============================================================ // types.go — utility functions // ============================================================ func TestConfigInt_Int_Cov10(t *testing.T) { assert.Equal(t, 993, configInt(channelpkg.ChannelConfig{"port": 993}, "port", 0)) } func TestConfigInt_Int64_Cov10(t *testing.T) { assert.Equal(t, 993, configInt(channelpkg.ChannelConfig{"port": int64(993)}, "port", 0)) } func TestConfigInt_Float64_Cov10(t *testing.T) { assert.Equal(t, 993, configInt(channelpkg.ChannelConfig{"port": float64(993)}, "port", 0)) } func TestConfigInt_String_Cov10(t *testing.T) { assert.Equal(t, 993, configInt(channelpkg.ChannelConfig{"port": "993"}, "port", 0)) } func TestConfigInt_InvalidString_Cov10(t *testing.T) { assert.Equal(t, 0, configInt(channelpkg.ChannelConfig{"port": "abc"}, "port", 0)) } func TestConfigInt_Missing_Cov10(t *testing.T) { assert.Equal(t, 993, configInt(channelpkg.ChannelConfig{}, "port", 993)) } func TestConfigInt_Bool_Cov10(t *testing.T) { assert.Equal(t, 0, configInt(channelpkg.ChannelConfig{"port": true}, "port", 0)) } func TestHTMLToPlainText_Basic_Cov10(t *testing.T) { result := htmlToPlainText("Hello & goodbye
") assert.Contains(t, result, "Hello & goodbye") } func TestHTMLToPlainText_Nbsp_Cov10(t *testing.T) { result := htmlToPlainText(" hello ") assert.Contains(t, result, "hello") } func TestHTMLToPlainText_Empty_Cov10(t *testing.T) { result := htmlToPlainText("") assert.Equal(t, "", result) } func TestStripHTMLTags_Basic_Cov10(t *testing.T) { assert.Contains(t, stripHTMLTags("Hello
"), "Hello") } func TestStripHTMLTags_Nested_Cov10(t *testing.T) { result := stripHTMLTags("Hello
HTML content
", } msg, err := convertToIncomingMessage(emailMsg, inbox) require.NoError(t, err) assert.Contains(t, msg.Content, "HTML content") } func TestConvertToIncomingMessage_NoContent_Cov10(t *testing.T) { inbox := &model.Inbox{Base: model.Base{ID: 1}} emailMsg := &EmailMessage{ MessageID: "msg1", FromAddress: "sender@test.com", } msg, err := convertToIncomingMessage(emailMsg, inbox) require.NoError(t, err) assert.Equal(t, "(no content)", msg.Content) } func TestConvertToIncomingMessage_WithAttachments_Cov10(t *testing.T) { inbox := &model.Inbox{Base: model.Base{ID: 1}} emailMsg := &EmailMessage{ MessageID: "msg1", FromAddress: "sender@test.com", TextContent: "see attachment", Attachments: []EmailAttachment{ {Filename: "file.pdf", ContentType: "application/pdf"}, }, } msg, err := convertToIncomingMessage(emailMsg, inbox) require.NoError(t, err) assert.Len(t, msg.Attachments, 1) } func TestConvertToIncomingMessage_WithInReplyTo_Cov10(t *testing.T) { inbox := &model.Inbox{Base: model.Base{ID: 1}} emailMsg := &EmailMessage{ MessageID: "msg1", FromAddress: "sender@test.com", TextContent: "hello", InReplyTo: "parent-msg", } msg, err := convertToIncomingMessage(emailMsg, inbox) require.NoError(t, err) assert.Equal(t, "parent-msg", msg.ReplyToID) } // ============================================================ // provider.go — SetPipeline // ============================================================ func TestSetPipeline_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) ip := NewIncomingProcessor(nil) p.SetPipeline(ip) assert.NotNil(t, p.pipeline) } // ============================================================ // provider.go — OnDestroy // ============================================================ func TestOnDestroy_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{Base: model.Base{ID: 1}} err := p.OnDestroy(context.Background(), inbox, channelpkg.ChannelConfig{}) require.NoError(t, err) } func TestOnDestroy_NilInbox_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) safeCallCov10(func() { _ = p.OnDestroy(context.Background(), nil, channelpkg.ChannelConfig{}) }) } // ============================================================ // provider.go — ValidateWebhookRequest // ============================================================ func TestValidateWebhookRequest_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{} err := p.ValidateWebhookRequest(context.Background(), inbox, &channelpkg.WebhookRequest{}) require.NoError(t, err) } func TestValidateWebhookRequest_NilRequest_Cov10(t *testing.T) { p := NewEmailProvider(nil, nil, nil) inbox := &model.Inbox{} safeCallCov10(func() { _ = p.ValidateWebhookRequest(context.Background(), inbox, nil) }) } // ============================================================ // Repository — PollState table name // ============================================================ func TestPollState_TableName_Cov10(t *testing.T) { assert.Equal(t, "email_poll_states", PollState{}.TableName()) } // ============================================================ // Mock types // ============================================================ type mockEmailHandlerCov10 struct{} func (m *mockEmailHandlerCov10) ProcessIncoming(ctx context.Context, inbox *model.Inbox, rawPayload []byte) (*channelpkg.IncomingMessage, error) { return &channelpkg.IncomingMessage{Content: "test"}, nil } type mockEmailOutgoingHandlerCov10 struct{} func (m *mockEmailOutgoingHandlerCov10) SendMessage(ctx context.Context, inbox *model.Inbox, message *model.Message, contact *model.Contact) (*channelpkg.SendResult, error) { return &channelpkg.SendResult{ExternalID: "test_msg_id"}, nil }