package bridge
import (
crand "crypto/rand"
"fmt"
"strings"
"testing"
bridgesmtp "next.orly.dev/pkg/bridge/smtp"
)
type mockBlossom struct {
uploadedData []byte
returnURL string
returnErr error
}
func (m *mockBlossom) Upload(data []byte, contentType string) (string, error) {
m.uploadedData = data
return m.returnURL, m.returnErr
}
func TestNewInboundProcessor(t *testing.T) {
ip := NewInboundProcessor(nil, "https://example.com/compose", func(string, string) error { return nil })
if ip == nil {
t.Fatal("expected non-nil InboundProcessor")
}
}
func TestProcessInbound_PlainText(t *testing.T) {
var sentDM string
var sentTo string
sendDM := func(pubkey, content string) error {
sentTo = pubkey
sentDM = content
return nil
}
ip := NewInboundProcessor(nil, "https://relay.example.com/compose", sendDM)
raw := "From: alice@example.com\r\nTo: npub1test@bridge.example.com\r\n" +
"Subject: Hello World\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"This is the body."
email := &bridgesmtp.InboundEmail{
From: "alice@example.com",
To: []string{"npub1test@bridge.example.com"},
RawMessage: []byte(raw),
}
err := ip.ProcessInbound(email, "abcd1234")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if sentTo != "abcd1234" {
t.Errorf("sentTo = %q, want abcd1234", sentTo)
}
if !strings.Contains(sentDM, "From: alice@example.com") {
t.Errorf("DM missing From header: %q", sentDM)
}
if !strings.Contains(sentDM, "Subject: Hello World") {
t.Errorf("DM missing Subject header: %q", sentDM)
}
if !strings.Contains(sentDM, "This is the body.") {
t.Errorf("DM missing body: %q", sentDM)
}
if !strings.Contains(sentDM, "Reply: https://relay.example.com/compose#to=alice%40example.com") {
t.Errorf("DM missing reply link: %q", sentDM)
}
}
func TestProcessInbound_WithAttachments(t *testing.T) {
blossom := &mockBlossom{returnURL: "https://blossom.example.com/abc123"}
var sentDM string
sendDM := func(pubkey, content string) error {
sentDM = content
return nil
}
ip := NewInboundProcessor(blossom, "", sendDM)
raw := "From: alice@example.com\r\nTo: npub1test@bridge.example.com\r\n" +
"Subject: With Attachment\r\n" +
"Content-Type: multipart/mixed; boundary=boundary123\r\n\r\n" +
"--boundary123\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"Body text here.\r\n" +
"--boundary123\r\n" +
"Content-Type: application/pdf\r\n" +
"Content-Disposition: attachment; filename=\"doc.pdf\"\r\n\r\n" +
"PDFCONTENT\r\n" +
"--boundary123--\r\n"
email := &bridgesmtp.InboundEmail{
From: "alice@example.com",
To: []string{"npub1test@bridge.example.com"},
RawMessage: []byte(raw),
}
err := ip.ProcessInbound(email, "abcd1234")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(sentDM, "Attachment: https://blossom.example.com/abc123#") {
t.Errorf("DM missing attachment URL with fragment key: %q", sentDM)
}
if blossom.uploadedData == nil {
t.Error("expected blossom upload to be called")
}
}
func TestProcessInbound_NoBlossom(t *testing.T) {
var sentDM string
sendDM := func(pubkey, content string) error {
sentDM = content
return nil
}
ip := NewInboundProcessor(nil, "", sendDM)
raw := "From: alice@example.com\r\nTo: npub1test@bridge.example.com\r\n" +
"Subject: With HTML\r\n" +
"Content-Type: multipart/alternative; boundary=b1\r\n\r\n" +
"--b1\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"Plain text.\r\n" +
"--b1\r\n" +
"Content-Type: text/html\r\n\r\n" +
"
HTML
\r\n" +
"--b1--\r\n"
email := &bridgesmtp.InboundEmail{
From: "alice@example.com",
To: []string{"npub1test@bridge.example.com"},
RawMessage: []byte(raw),
}
err := ip.ProcessInbound(email, "abcd1234")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// No blossom = no attachment in DM
if strings.Contains(sentDM, "Attachment:") {
t.Errorf("should not have attachment when no Blossom: %q", sentDM)
}
}
func TestProcessInbound_HTMLOnly(t *testing.T) {
blossom := &mockBlossom{returnURL: "https://blossom.example.com/xyz"}
var sentDM string
sendDM := func(pubkey, content string) error {
sentDM = content
return nil
}
ip := NewInboundProcessor(blossom, "", sendDM)
raw := "From: alice@example.com\r\nTo: npub1test@bridge.example.com\r\n" +
"Subject: HTML Only\r\n" +
"Content-Type: text/html\r\n\r\n" +
"Hello
"
email := &bridgesmtp.InboundEmail{
From: "alice@example.com",
To: []string{"npub1test@bridge.example.com"},
RawMessage: []byte(raw),
}
err := ip.ProcessInbound(email, "abcd1234")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(sentDM, "[HTML-only email") {
t.Errorf("expected HTML-only fallback text: %q", sentDM)
}
}
func TestProcessInbound_BlossomUploadError(t *testing.T) {
blossom := &mockBlossom{returnErr: fmt.Errorf("upload failed")}
var sentDM string
sendDM := func(pubkey, content string) error {
sentDM = content
return nil
}
ip := NewInboundProcessor(blossom, "", sendDM)
raw := "From: alice@example.com\r\nTo: npub1test@bridge.example.com\r\n" +
"Subject: Error\r\n" +
"Content-Type: multipart/mixed; boundary=err1\r\n\r\n" +
"--err1\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"Body.\r\n" +
"--err1\r\n" +
"Content-Type: application/pdf\r\n" +
"Content-Disposition: attachment; filename=\"doc.pdf\"\r\n\r\n" +
"PDF\r\n" +
"--err1--\r\n"
email := &bridgesmtp.InboundEmail{
From: "alice@example.com",
To: []string{"npub1test@bridge.example.com"},
RawMessage: []byte(raw),
}
err := ip.ProcessInbound(email, "abcd1234")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(sentDM, "[processing failed]") {
t.Errorf("expected processing failed message: %q", sentDM)
}
}
func TestProcessInbound_SendDMError(t *testing.T) {
sendDM := func(pubkey, content string) error {
return fmt.Errorf("DM send failed")
}
ip := NewInboundProcessor(nil, "", sendDM)
raw := "From: alice@example.com\r\nTo: npub1test@bridge.example.com\r\n" +
"Subject: Test\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"Body."
email := &bridgesmtp.InboundEmail{
From: "alice@example.com",
To: []string{"npub1test@bridge.example.com"},
RawMessage: []byte(raw),
}
err := ip.ProcessInbound(email, "abcd1234")
if err == nil {
t.Fatal("expected error")
}
}
func TestProcessInbound_InvalidMIME(t *testing.T) {
sendDM := func(pubkey, content string) error { return nil }
ip := NewInboundProcessor(nil, "", sendDM)
// Truly malformed input that go-message cannot parse at all
email := &bridgesmtp.InboundEmail{
From: "alice@example.com",
To: []string{"npub1test@bridge.example.com"},
RawMessage: []byte("Content-Type: multipart/mixed; boundary=xyz\r\n\r\n--abc\r\n"),
}
err := ip.ProcessInbound(email, "abcd1234")
if err == nil {
t.Fatal("expected error from invalid MIME")
}
}
func TestProcessInbound_NoComposeURL(t *testing.T) {
var sentDM string
sendDM := func(pubkey, content string) error {
sentDM = content
return nil
}
ip := NewInboundProcessor(nil, "", sendDM)
raw := "From: alice@example.com\r\nTo: npub1test@bridge.example.com\r\n" +
"Subject: No Reply Link\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"Body."
email := &bridgesmtp.InboundEmail{
From: "alice@example.com",
To: []string{"npub1test@bridge.example.com"},
RawMessage: []byte(raw),
}
err := ip.ProcessInbound(email, "abcd1234")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if strings.Contains(sentDM, "Reply:") {
t.Errorf("should not have Reply link without composeURL: %q", sentDM)
}
}
func TestProcessAttachments_ZipError(t *testing.T) {
// Feed incompressible data exceeding the 25MB zip limit
blossom := &mockBlossom{returnURL: "https://blossom.example.com/test"}
ip := &InboundProcessor{blossom: blossom}
bigData := make([]byte, 26*1024*1024)
// crypto/rand data is incompressible — zip output will be >= input
crand.Read(bigData)
url, err := ip.processAttachments("", []bridgesmtp.Attachment{
{Filename: "huge.bin", ContentType: "application/octet-stream", Data: bigData},
})
if err == nil {
t.Fatal("expected error from zip size exceeding limit")
}
if url != "" {
t.Errorf("expected empty URL on error, got %q", url)
}
}
func TestProcessAttachments_Empty(t *testing.T) {
blossom := &mockBlossom{returnURL: "https://blossom.example.com/test"}
ip := &InboundProcessor{blossom: blossom}
url, err := ip.processAttachments("", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if url != "" {
t.Errorf("expected empty URL for no content, got %q", url)
}
}