Files
MagnumOpus/Packages/MagnumOpusCore/Tests/SMTPClientTests/MessageFormatterTests.swift

290 lines
9.4 KiB
Swift

import Testing
import Foundation
@testable import SMTPClient
import Models
@Suite("MessageFormatter")
struct MessageFormatterTests {
// MARK: - Basic Message Formatting
@Test("formatted message contains all required headers")
func basicHeaders() {
let message = OutgoingMessage(
from: EmailAddress(name: "Alice", address: "alice@example.com"),
to: [EmailAddress(address: "bob@example.com")],
subject: "Hello",
bodyText: "Hi Bob",
messageId: "test-id@example.com"
)
let formatted = MessageFormatter.format(message)
#expect(formatted.contains("From: \"Alice\" <alice@example.com>"))
#expect(formatted.contains("To: bob@example.com"))
#expect(formatted.contains("Subject: Hello"))
#expect(formatted.contains("Message-ID: <test-id@example.com>"))
#expect(formatted.contains("MIME-Version: 1.0"))
#expect(formatted.contains("Content-Type: text/plain; charset=utf-8"))
#expect(formatted.contains("Content-Transfer-Encoding: quoted-printable"))
#expect(formatted.contains("Date: "))
}
// MARK: - Reply Headers
@Test("reply includes In-Reply-To and References headers")
func replyHeaders() {
let message = OutgoingMessage(
from: EmailAddress(address: "alice@example.com"),
to: [EmailAddress(address: "bob@example.com")],
subject: "Re: Hello",
bodyText: "Thanks!",
inReplyTo: "original-id@example.com",
references: "<original-id@example.com>",
messageId: "reply-id@example.com"
)
let formatted = MessageFormatter.format(message)
#expect(formatted.contains("In-Reply-To: <original-id@example.com>"))
#expect(formatted.contains("References: <original-id@example.com>"))
}
// MARK: - BCC Omitted
@Test("BCC recipients are not included in formatted output")
func bccOmitted() {
let message = OutgoingMessage(
from: EmailAddress(address: "alice@example.com"),
to: [EmailAddress(address: "bob@example.com")],
bcc: [EmailAddress(address: "secret@example.com")],
subject: "Test",
bodyText: "Body",
messageId: "test@example.com"
)
let formatted = MessageFormatter.format(message)
#expect(!formatted.contains("Bcc"))
#expect(!formatted.contains("secret@example.com"))
}
// MARK: - Multiple CC Recipients
@Test("multiple CC recipients formatted correctly")
func multipleCc() {
let message = OutgoingMessage(
from: EmailAddress(address: "alice@example.com"),
to: [EmailAddress(address: "bob@example.com")],
cc: [
EmailAddress(name: "Carol", address: "carol@example.com"),
EmailAddress(address: "dave@example.com"),
],
subject: "Test",
bodyText: "Body",
messageId: "test@example.com"
)
let formatted = MessageFormatter.format(message)
#expect(formatted.contains("Cc: \"Carol\" <carol@example.com>, dave@example.com"))
}
// MARK: - Quoted-Printable Encoding
@Test("non-ASCII characters are quoted-printable encoded")
func quotedPrintableNonAscii() {
let encoded = MessageFormatter.quotedPrintableEncode("Grüße")
// ü = C3 BC, ß = C3 9F, e is plain
#expect(encoded.contains("=C3=BC")) // ü
#expect(encoded.contains("=C3=9F")) // ß
#expect(encoded.contains("Gr"))
#expect(encoded.contains("e"))
}
@Test("equals sign is encoded in quoted-printable")
func quotedPrintableEquals() {
let encoded = MessageFormatter.quotedPrintableEncode("a=b")
#expect(encoded == "a=3Db")
}
@Test("plain ASCII text passes through unchanged")
func quotedPrintableAscii() {
let encoded = MessageFormatter.quotedPrintableEncode("Hello World")
#expect(encoded == "Hello World")
}
// MARK: - Domain Extraction
@Test("domain extracted from email address")
func domainExtraction() {
#expect(MessageFormatter.domainFromEmail("alice@example.com") == "example.com")
#expect(MessageFormatter.domainFromEmail("user@sub.domain.org") == "sub.domain.org")
}
@Test("domain extraction handles missing @ gracefully")
func domainExtractionNoAt() {
#expect(MessageFormatter.domainFromEmail("noatsign") == "noatsign")
}
// MARK: - Message-ID Generation
@Test("generated message ID contains domain")
func messageIdGeneration() {
let id = MessageFormatter.generateMessageId(domain: "example.com")
#expect(id.hasSuffix("@example.com"))
#expect(id.count > "@example.com".count)
}
@Test("generated message IDs are unique")
func messageIdUniqueness() {
let id1 = MessageFormatter.generateMessageId(domain: "example.com")
let id2 = MessageFormatter.generateMessageId(domain: "example.com")
#expect(id1 != id2)
}
// MARK: - Address Formatting
@Test("address with name formatted as quoted name angle-bracket")
func addressWithName() {
let addr = EmailAddress(name: "Alice Smith", address: "alice@example.com")
#expect(MessageFormatter.formatAddress(addr) == "\"Alice Smith\" <alice@example.com>")
}
@Test("address without name formatted as bare address")
func addressWithoutName() {
let addr = EmailAddress(address: "alice@example.com")
#expect(MessageFormatter.formatAddress(addr) == "alice@example.com")
}
@Test("address with empty name formatted as bare address")
func addressEmptyName() {
let addr = EmailAddress(name: "", address: "alice@example.com")
#expect(MessageFormatter.formatAddress(addr) == "alice@example.com")
}
// MARK: - RFC 2822 Date
@Test("RFC 2822 date format is valid")
func rfc2822DateFormat() {
let dateString = MessageFormatter.formatRFC2822Date(Date())
// Should match pattern like "Fri, 14 Mar 2026 10:30:00 +0100"
#expect(dateString.contains(","))
#expect(dateString.count > 20)
}
// MARK: - Multipart Formatting
@Test("format with attachments produces multipart/mixed")
func multipartWithAttachment() throws {
let message = OutgoingMessage(
from: EmailAddress(address: "alice@example.com"),
to: [EmailAddress(address: "bob@example.com")],
subject: "With attachment",
bodyText: "See attached.",
messageId: "test@example.com"
)
let attachments: [(filename: String, mimeType: String, data: Data)] = [
(filename: "test.pdf", mimeType: "application/pdf", data: Data("PDF content".utf8)),
]
let formatted = try MessageFormatter.formatMultipart(message, attachments: attachments)
#expect(formatted.contains("Content-Type: multipart/mixed; boundary="))
#expect(formatted.contains("Content-Type: text/plain; charset=utf-8"))
#expect(formatted.contains("Content-Type: application/pdf; name=\"test.pdf\""))
#expect(formatted.contains("Content-Disposition: attachment; filename=\"test.pdf\""))
#expect(formatted.contains("Content-Transfer-Encoding: base64"))
#expect(formatted.contains("See attached."))
}
@Test("format without attachments produces single-part")
func singlePartNoAttachments() {
let message = OutgoingMessage(
from: EmailAddress(address: "alice@example.com"),
to: [EmailAddress(address: "bob@example.com")],
subject: "Plain",
bodyText: "Just text.",
messageId: "test@example.com"
)
let formatted = MessageFormatter.format(message)
#expect(formatted.contains("Content-Type: text/plain; charset=utf-8"))
#expect(!formatted.contains("multipart"))
}
@Test("base64 encoding wraps at 76 characters")
func base64LineWrapping() {
let data = Data(repeating: 0xFF, count: 100)
let encoded = MessageFormatter.base64Encode(data)
let lines = encoded.components(separatedBy: "\r\n")
for line in lines where !line.isEmpty {
#expect(line.count <= 76)
}
}
@Test("formatMultipart throws for file exceeding 25 MB")
func attachmentSizeGuard() {
let message = OutgoingMessage(
from: EmailAddress(address: "alice@example.com"),
to: [EmailAddress(address: "bob@example.com")],
subject: "Big file",
bodyText: "See attached.",
messageId: "test@example.com"
)
// 26 MB attachment
let bigData = Data(repeating: 0xFF, count: 26 * 1024 * 1024)
let attachments: [(filename: String, mimeType: String, data: Data)] = [
(filename: "huge.bin", mimeType: "application/octet-stream", data: bigData),
]
#expect(throws: MessageFormatterError.self) {
try MessageFormatter.formatMultipart(message, attachments: attachments)
}
}
@Test func formatMultipartWithAttachments() throws {
let msg = OutgoingMessage(
from: EmailAddress(name: "Test", address: "test@example.com"),
to: [EmailAddress(name: "To", address: "to@example.com")],
subject: "With attachment",
bodyText: "Hello",
messageId: "test-123",
attachments: [
OutgoingAttachment(filename: "test.txt", mimeType: "text/plain", data: Data("file content".utf8))
]
)
let formatted = try MessageFormatter.formatMultipart(
msg,
attachments: msg.attachments.map { ($0.filename, $0.mimeType, $0.data) }
)
#expect(formatted.contains("multipart/mixed"))
#expect(formatted.contains("test.txt"))
#expect(formatted.contains("Hello"))
}
@Test("multiple attachments produce correct number of boundary sections")
func multipleAttachments() throws {
let message = OutgoingMessage(
from: EmailAddress(address: "alice@example.com"),
to: [EmailAddress(address: "bob@example.com")],
subject: "Multi",
bodyText: "Files.",
messageId: "test@example.com"
)
let attachments: [(filename: String, mimeType: String, data: Data)] = [
(filename: "a.pdf", mimeType: "application/pdf", data: Data("A".utf8)),
(filename: "b.jpg", mimeType: "image/jpeg", data: Data("B".utf8)),
]
let formatted = try MessageFormatter.formatMultipart(message, attachments: attachments)
#expect(formatted.contains("a.pdf"))
#expect(formatted.contains("b.jpg"))
#expect(formatted.contains("application/pdf"))
#expect(formatted.contains("image/jpeg"))
}
}