Modernized unit tests

This commit is contained in:
Oliver Drobnik
2025-06-07 20:51:21 +02:00
parent 904ef4ec2f
commit ae7f7b3f7a
5 changed files with 318 additions and 104 deletions
+59 -15
View File
@@ -2,6 +2,18 @@ import Foundation
import Testing
@testable import SwiftMail
// Use existing tag definitions and add new ones
extension Tag {
@Tag static var encoding: Self
@Tag static var decoding: Self
@Tag static var imap: Self
@Tag static var performance: Self
@Tag static var mime: Self
@Tag static var fileHandling: Self
@Tag static var security: Self
}
@Suite("Quoted-Printable Encoding Tests", .tags(.imap, .encoding, .decoding))
struct QuotedPrintableTests {
// MARK: - Test Resources
@@ -24,8 +36,8 @@ struct QuotedPrintableTests {
// MARK: - Basic Decoding Tests
@Test
func testBasicQuotedPrintableDecoding() {
@Test("Basic quoted-printable decoding", .tags(.decoding))
func basicQuotedPrintableDecoding() {
// Test basic quoted-printable decoding
let encoded = "Hello=20World"
#expect(encoded.decodeQuotedPrintable() == "Hello World")
@@ -43,8 +55,8 @@ struct QuotedPrintableTests {
#expect(equalsSign.decodeQuotedPrintable() == "3=2+1")
}
@Test
func testEncodingDetection() {
@Test("Encoding detection", .tags(.encoding))
func encodingDetection() {
// Test ISO-8859-1 encoding detection
let isoContent = "Content-Type: text/plain; charset=iso-8859-1\r\n\r\nThis has special chars: =E4=F6=FC=DF"
#expect(isoContent.detectCharsetEncoding() == .isoLatin1)
@@ -62,9 +74,8 @@ struct QuotedPrintableTests {
#expect(noCharset.detectCharsetEncoding() == .utf8)
}
// Skip this test for now as the implementation might be different
@Test
func testAutoDetectionDecoding() {
@Test("Auto-detection decoding", .tags(.decoding))
func autoDetectionDecoding() {
// Simple test with basic content
let basicContent = "Hello=20World"
#expect(basicContent.decodeQuotedPrintable() == "Hello World")
@@ -72,8 +83,8 @@ struct QuotedPrintableTests {
// MARK: - MIME Header Tests
@Test
func testMIMEHeaderDecoding() {
@Test("MIME header decoding", .tags(.decoding, .mime))
func mimeHeaderDecoding() {
// Test Q-encoded header
let qEncoded = "=?UTF-8?Q?Hello=20World?="
#expect(qEncoded.decodeMIMEHeader() == "Hello World")
@@ -94,8 +105,8 @@ struct QuotedPrintableTests {
// MARK: - HTML File Tests
@Test
func testISO8859HTMLFile() throws {
@Test("ISO-8859-1 HTML file processing", .tags(.fileHandling))
func iso8859HTMLFile() throws {
let content = try loadResourceContent(name: "sample_quoted_printable", withExtension: "html")
// Test that the content was loaded
@@ -109,8 +120,8 @@ struct QuotedPrintableTests {
#expect(simpleTest.decodeQuotedPrintable() == "Hello World")
}
@Test
func testUTF8HTMLFile() throws {
@Test("UTF-8 HTML file processing", .tags(.fileHandling))
func utf8HTMLFile() throws {
let content = try loadResourceContent(name: "sample_quoted_printable_utf8", withExtension: "html")
// Test that the content was loaded
@@ -124,8 +135,8 @@ struct QuotedPrintableTests {
#expect(simpleTest.decodeQuotedPrintable() == "Hello World")
}
@Test
func testMIMEHeaderFile() throws {
@Test("MIME header file processing", .tags(.fileHandling, .mime))
func mimeHeaderFile() throws {
let content = try loadResourceContent(name: "sample_mime_header", withExtension: "txt")
// Split the content into lines
@@ -155,6 +166,39 @@ struct QuotedPrintableTests {
throw TestFailure("Subject header not found")
}
}
// MARK: - Additional Edge Cases
@Test("Edge cases and malformed input", .tags(.decoding, .security))
func edgeCasesAndMalformedInput() {
// Test empty string
#expect("".decodeQuotedPrintable() == "")
// Test string without encoding
#expect("Plain text".decodeQuotedPrintable() == "Plain text")
// Test malformed encoding (incomplete hex) - should return nil for invalid input
let malformed = "Hello=2World" // Missing second hex digit
let result = malformed.decodeQuotedPrintable()
#expect(result == nil, "Should return nil for malformed input")
// Test with invalid hex characters - should return nil for invalid input
let invalidHex = "Hello=ZZ"
let invalidResult = invalidHex.decodeQuotedPrintable()
#expect(invalidResult == nil, "Should return nil for invalid hex")
}
@Test("Performance with large content", .tags(.performance, .decoding))
func performanceWithLargeContent() {
// Create a large quoted-printable encoded string
let baseString = "This=20is=20a=20test=20string=20with=20spaces=0D=0A"
let largeContent = String(repeating: baseString, count: 1000)
// Test that decoding completes without hanging
let decoded = largeContent.decodeQuotedPrintable()
#expect(decoded != nil, "Should decode large content successfully")
#expect(decoded?.contains("This is a test string with spaces") ?? false, "Should contain expected decoded content")
}
}
// Custom error type for test failures
+142 -41
View File
@@ -1,51 +1,152 @@
// String+EmailTests.swift
// Tests for email validation String extension
import XCTest
import Testing
@testable import SwiftMail
final class StringEmailTests: XCTestCase {
func testValidEmails() {
// Test basic valid formats
XCTAssertTrue("user@example.com".isValidEmail())
XCTAssertTrue("user.name@example.com".isValidEmail())
XCTAssertTrue("user+tag@example.com".isValidEmail())
XCTAssertTrue("user@subdomain.example.com".isValidEmail())
XCTAssertTrue("123@example.com".isValidEmail())
XCTAssertTrue("user@example.co.uk".isValidEmail())
// Test edge cases that should be valid
XCTAssertTrue("a@b.cc".isValidEmail()) // Minimal length
XCTAssertTrue("disposable.style.email.with+symbol@example.com".isValidEmail())
XCTAssertTrue("other.email-with-hyphen@example.com".isValidEmail())
XCTAssertTrue("fully-qualified-domain@example.com".isValidEmail())
XCTAssertTrue("user.name+tag+sorting@example.com".isValidEmail())
XCTAssertTrue("x@example.com".isValidEmail()) // One-letter local-part
XCTAssertTrue("example-indeed@strange-example.com".isValidEmail())
XCTAssertTrue("example@s.example".isValidEmail()) // Short but valid domain
// Use existing tag definitions
extension Tag {
@Tag static var validation: Self
@Tag static var security: Self
}
@Suite("String Email Validation Tests", .tags(.core, .validation))
struct StringEmailTests {
@Test("Valid email addresses should pass validation", .tags(.validation),
arguments: [
"user@example.com",
"user.name@example.com",
"user+tag@example.com",
"user@subdomain.example.com",
"123@example.com",
"user@example.co.uk",
"a@b.cc", // Minimal length
"disposable.style.email.with+symbol@example.com",
"other.email-with-hyphen@example.com",
"fully-qualified-domain@example.com",
"user.name+tag+sorting@example.com",
"x@example.com", // One-letter local-part
"example-indeed@strange-example.com",
"example@s.example", // Short but valid domain
"test.email.with+symbol@example.com",
"user123@test-domain.org"
])
func validEmails(email: String) {
#expect(email.isValidEmail(), "'\(email)' should be a valid email address")
}
func testInvalidEmails() {
// Test basic invalid formats
XCTAssertFalse("".isValidEmail())
XCTAssertFalse("@example.com".isValidEmail())
XCTAssertFalse("user@".isValidEmail())
XCTAssertFalse("user@.com".isValidEmail())
XCTAssertFalse("user@example".isValidEmail())
XCTAssertFalse("user.example.com".isValidEmail())
@Test("Invalid email addresses should fail validation", .tags(.validation, .security),
arguments: [
"",
"@example.com",
"user@",
"user@.com",
"user@example",
"user.example.com",
"user@exam ple.com", // Space in domain
"user@@example.com", // Double @
".user@example.com", // Leading dot
"user.@example.com", // Trailing dot
"user@example..com", // Double dot
"user@-example.com", // Leading hyphen in domain
"user@example-.com", // Trailing hyphen in domain
"user@.example.com", // Leading dot in domain
"user@example.", // Trailing dot in domain
"user@ex*ample.com", // Invalid character
"user@example.c", // TLD too short
"user name@example.com", // Space in local part
"user<script>@example.com", // Security: HTML injection attempt
"user@example.com; DROP TABLE users;", // Security: SQL injection attempt
"user@192.168.1.1", // IP addresses without proper format
"user@[192.168.1.1", // Malformed IP bracket
"user@192.168.1.1]" // Malformed IP bracket
])
func invalidEmails(email: String) {
#expect(!email.isValidEmail(), "'\(email)' should be an invalid email address")
}
@Test("Edge cases for email validation")
func emailEdgeCases() {
// Test specific edge cases individually for better diagnostics
#expect("a@b.cc".isValidEmail(), "Minimal valid email should pass")
#expect(!"".isValidEmail(), "Empty string should be invalid")
#expect(!"@".isValidEmail(), "Single @ symbol should be invalid")
#expect(!"user@@example.com".isValidEmail(), "Double @ should be invalid")
#expect(!"user@example..com".isValidEmail(), "Double dots in domain should be invalid")
}
@Test("International domain names", .tags(.validation))
func internationalDomains() {
// These might be valid depending on implementation
let internationalEmails = [
"user@münchen.de",
"test@café.com",
"email@測試.com"
]
// Test invalid characters and formats
XCTAssertFalse("user@exam ple.com".isValidEmail()) // Space in domain
XCTAssertFalse("user@@example.com".isValidEmail()) // Double @
XCTAssertFalse(".user@example.com".isValidEmail()) // Leading dot
XCTAssertFalse("user.@example.com".isValidEmail()) // Trailing dot
XCTAssertFalse("user@example..com".isValidEmail()) // Double dot
XCTAssertFalse("user@-example.com".isValidEmail()) // Leading hyphen in domain
XCTAssertFalse("user@example-.com".isValidEmail()) // Trailing hyphen in domain
XCTAssertFalse("user@.example.com".isValidEmail()) // Leading dot in domain
XCTAssertFalse("user@example.".isValidEmail()) // Trailing dot in domain
XCTAssertFalse("user@ex*ample.com".isValidEmail()) // Invalid character
XCTAssertFalse("user@example.c".isValidEmail()) // TLD too short
XCTAssertFalse("user name@example.com".isValidEmail()) // Space in local part
for email in internationalEmails {
let result = email.isValidEmail()
// Implementation might vary on international domain support
// Just verify that the function doesn't crash and returns a boolean
#expect(result is Bool, "Email validation should return a boolean for '\(email)'")
}
}
@Test("Security and injection protection", .tags(.security))
func securityTests() {
let maliciousInputs = [
"user<script>alert('xss')</script>@example.com",
"user'; DROP TABLE users; --@example.com",
"user@example.com\\r\\nBCC: evil@hacker.com",
"user@example.com\\nSubject: Spam",
"user\\u{0000}@example.com", // Null byte injection
"user@exam\\u{0000}ple.com"
]
for maliciousEmail in maliciousInputs {
#expect(!maliciousEmail.isValidEmail(),
"Malicious input '\(maliciousEmail)' should be rejected")
}
}
@Test("Performance with long inputs", .tags(.validation))
func performanceLongInputs() {
// Test very long email addresses
let longLocalPart = String(repeating: "a", count: 1000)
let longEmail = "\(longLocalPart)@example.com"
// Should handle long inputs gracefully (likely invalid due to length)
let result = longEmail.isValidEmail()
#expect(!result, "Extremely long email should be invalid")
// Test long domain
let longDomain = String(repeating: "test.", count: 50) + "com"
let longDomainEmail = "user@\(longDomain)"
let domainResult = longDomainEmail.isValidEmail()
// Just verify it handles the input without crashing
#expect(domainResult is Bool, "Email validation should handle long domains gracefully")
}
@Test("Unicode and special character handling", .tags(.validation))
func unicodeHandling() {
let unicodeEmails = [
"用户@example.com", // Chinese characters in local part
"user@раздел.com", // Cyrillic in domain
"tëst@example.com", // Accented characters
"user+tag@example.com", // Plus sign (should be valid)
"user=tag@example.com", // Equals sign
"user%tag@example.com" // Percent sign
]
for email in unicodeEmails {
let result = email.isValidEmail()
// Implementation may vary on unicode support
// Just verify that the function doesn't crash and returns a boolean
#expect(result is Bool, "Email validation should return a boolean for '\(email)'")
}
// Test that plus sign is typically allowed (common use case)
#expect("user+tag@example.com".isValidEmail(), "Plus sign should typically be allowed in email addresses")
}
}
@@ -1,38 +1,43 @@
// String+HostnameTests.swift
// Tests for hostname-related String extensions
import XCTest
import Testing
import Foundation
@testable import SwiftMail
final class StringHostnameTests: XCTestCase {
func testLocalHostname() {
@Suite("String Hostname Extensions Tests")
struct StringHostnameTests {
@Test("Local hostname resolution returns valid hostname")
func localHostname() {
let hostname = String.localHostname
// Test that hostname is not empty
XCTAssertFalse(hostname.isEmpty)
#expect(!hostname.isEmpty, "Hostname should not be empty")
// Test that hostname is not the fallback value unless all other methods fail
if !hostname.hasPrefix("[") && !hostname.hasSuffix("]") {
// If it's not an IP address format, it should be a valid hostname
XCTAssertNotEqual(hostname, "localhost")
XCTAssertNotEqual(hostname, "swift-mail-client.local")
#expect(hostname != "localhost", "Should not default to localhost unless necessary")
#expect(hostname != "swift-mail-client.local", "Should not default to fallback unless necessary")
}
// Test hostname format
if hostname.hasPrefix("[") && hostname.hasSuffix("]") {
// IP address format
let ip = String(hostname.dropFirst().dropLast())
XCTAssertTrue(isValidIP(ip), "Invalid IP address format: \(ip)")
#expect(isValidIP(ip), "Invalid IP address format: \(ip)")
} else {
// Hostname format
XCTAssertTrue(isValidHostname(hostname), "Invalid hostname format: \(hostname)")
#expect(isValidHostname(hostname), "Invalid hostname format: \(hostname)")
}
}
func testLocalIPAddress() {
@Test("Local IP address resolution returns valid IP when available")
func localIPAddress() {
if let ipAddress = String.localIPAddress {
// Test that we got a valid IP address
XCTAssertTrue(isValidIP(ipAddress), "Invalid IP address format: \(ipAddress)")
#expect(isValidIP(ipAddress), "Invalid IP address format: \(ipAddress)")
}
// Note: We don't fail if no IP is found, as this might be legitimate in some environments
}
+89 -28
View File
@@ -1,57 +1,118 @@
// String+MIMETests.swift
// Tests for MIME-related String extensions
import XCTest
import Testing
@testable import SwiftMail
final class StringMIMETests: XCTestCase {
func testFileExtensionForMIMEType() {
// MARK: - Tag Definitions
extension Tag {
@Tag static var core: Self
@Tag static var mime: Self
@Tag static var fileHandling: Self
@Tag static var crossPlatform: Self
}
@Suite("String MIME Extensions Tests", .tags(.core, .mime, .fileHandling))
struct StringMIMETests {
@Test("File extension for MIME type resolution", .tags(.crossPlatform))
func fileExtensionForMIMEType() {
#if os(macOS)
// On macOS, we use UTType which might return different extensions
// We only test that we get a valid extension back
if let jpegExt = String.fileExtension(for: "image/jpeg") {
XCTAssertTrue(["jpg", "jpeg"].contains(jpegExt))
#expect(["jpg", "jpeg"].contains(jpegExt))
} else {
XCTFail("Failed to get extension for image/jpeg")
Issue.record("Failed to get extension for image/jpeg")
}
#else
// Test common MIME types
XCTAssertEqual(String.fileExtension(for: "image/jpeg"), "jpg")
XCTAssertEqual(String.fileExtension(for: "image/png"), "png")
XCTAssertEqual(String.fileExtension(for: "application/pdf"), "pdf")
XCTAssertEqual(String.fileExtension(for: "text/plain"), "txt")
XCTAssertEqual(String.fileExtension(for: "text/html"), "html")
#expect(String.fileExtension(for: "image/jpeg") == "jpg")
#expect(String.fileExtension(for: "image/png") == "png")
#expect(String.fileExtension(for: "application/pdf") == "pdf")
#expect(String.fileExtension(for: "text/plain") == "txt")
#expect(String.fileExtension(for: "text/html") == "html")
// Test Office document types
XCTAssertEqual(String.fileExtension(for: "application/msword"), "doc")
XCTAssertEqual(String.fileExtension(for: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"), "docx")
XCTAssertEqual(String.fileExtension(for: "application/vnd.ms-excel"), "xls")
#expect(String.fileExtension(for: "application/msword") == "doc")
#expect(String.fileExtension(for: "application/vnd.openxmlformats-officedocument.wordprocessingml.document") == "docx")
#expect(String.fileExtension(for: "application/vnd.ms-excel") == "xls")
#endif
// Test unknown MIME type (should work the same on all platforms)
XCTAssertNil(String.fileExtension(for: "application/unknown"))
#expect(String.fileExtension(for: "application/unknown") == nil)
}
func testMIMETypeForFileExtension() {
@Test("MIME type for file extension resolution", .tags(.crossPlatform))
func mimeTypeForFileExtension() {
// Test common file extensions (should work the same on all platforms)
XCTAssertEqual(String.mimeType(for: "jpg"), "image/jpeg")
XCTAssertEqual(String.mimeType(for: "jpeg"), "image/jpeg")
XCTAssertEqual(String.mimeType(for: "png"), "image/png")
XCTAssertEqual(String.mimeType(for: "pdf"), "application/pdf")
XCTAssertEqual(String.mimeType(for: "txt"), "text/plain")
XCTAssertEqual(String.mimeType(for: "html"), "text/html")
XCTAssertEqual(String.mimeType(for: "htm"), "text/html")
#expect(String.mimeType(for: "jpg") == "image/jpeg")
#expect(String.mimeType(for: "jpeg") == "image/jpeg")
#expect(String.mimeType(for: "png") == "image/png")
#expect(String.mimeType(for: "pdf") == "application/pdf")
#expect(String.mimeType(for: "txt") == "text/plain")
#expect(String.mimeType(for: "html") == "text/html")
#expect(String.mimeType(for: "htm") == "text/html")
// Test Office file extensions
XCTAssertEqual(String.mimeType(for: "doc"), "application/msword")
XCTAssertEqual(String.mimeType(for: "docx"), "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
XCTAssertEqual(String.mimeType(for: "xls"), "application/vnd.ms-excel")
#expect(String.mimeType(for: "doc") == "application/msword")
#expect(String.mimeType(for: "docx") == "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
#expect(String.mimeType(for: "xls") == "application/vnd.ms-excel")
// Test case insensitivity
XCTAssertEqual(String.mimeType(for: "JPG"), "image/jpeg")
XCTAssertEqual(String.mimeType(for: "PDF"), "application/pdf")
#expect(String.mimeType(for: "JPG") == "image/jpeg")
#expect(String.mimeType(for: "PDF") == "application/pdf")
// Test unknown extension
XCTAssertEqual(String.mimeType(for: "unknown"), "application/octet-stream")
#expect(String.mimeType(for: "unknown") == "application/octet-stream")
}
@Test("Extended MIME type support", .tags(.fileHandling))
func extendedMIMETypes() {
// Test additional multimedia types - verify they return appropriate types
let mp4Type = String.mimeType(for: "mp4")
#expect(mp4Type.hasPrefix("video/"), "MP4 should be a video type, got: \(mp4Type)")
let mp3Type = String.mimeType(for: "mp3")
#expect(mp3Type.hasPrefix("audio/"), "MP3 should be an audio type, got: \(mp3Type)")
let wavType = String.mimeType(for: "wav")
#expect(wavType.hasPrefix("audio/"), "WAV should be an audio type, got: \(wavType)")
let gifType = String.mimeType(for: "gif")
#expect(gifType.hasPrefix("image/"), "GIF should be an image type, got: \(gifType)")
let bmpType = String.mimeType(for: "bmp")
#expect(bmpType.hasPrefix("image/"), "BMP should be an image type, got: \(bmpType)")
// Test archive types
let zipType = String.mimeType(for: "zip")
#expect(zipType.hasPrefix("application/"), "ZIP should be an application type, got: \(zipType)")
// Test development file types
let jsonType = String.mimeType(for: "json")
#expect(jsonType.hasPrefix("application/") || jsonType.hasPrefix("text/"),
"JSON should be application or text type, got: \(jsonType)")
let cssType = String.mimeType(for: "css")
#expect(cssType.hasPrefix("text/"), "CSS should be a text type, got: \(cssType)")
let jsType = String.mimeType(for: "js")
#expect(jsType.hasPrefix("application/") || jsType.hasPrefix("text/"),
"JavaScript should be application or text type, got: \(jsType)")
}
@Test("Edge cases for MIME type resolution")
func edgeCases() {
// Test empty string
#expect(String.mimeType(for: "") == "application/octet-stream")
// Test very long extension
let longExtension = String(repeating: "a", count: 100)
#expect(String.mimeType(for: longExtension) == "application/octet-stream")
// Test extension with special characters
#expect(String.mimeType(for: "file.exe") == "application/octet-stream")
#expect(String.mimeType(for: "test@test") == "application/octet-stream")
}
}
@@ -1,24 +1,27 @@
// String+UtilitiesTests.swift
// Tests for general String utilities
import XCTest
import Testing
@testable import SwiftMail
final class StringUtilitiesTests: XCTestCase {
func testSanitizedFileName() {
@Suite("String Utilities Tests")
struct StringUtilitiesTests {
@Test("Sanitized file name validation")
func sanitizedFileName() {
// Test valid filenames remain unchanged
XCTAssertEqual("document.txt".sanitizedFileName(), "document.txt")
XCTAssertEqual("image.jpg".sanitizedFileName(), "image.jpg")
#expect("document.txt".sanitizedFileName() == "document.txt")
#expect("image.jpg".sanitizedFileName() == "image.jpg")
// Test invalid characters are replaced
XCTAssertEqual("file:with/invalid\\chars?.txt".sanitizedFileName(), "file_with_invalid_chars_.txt")
XCTAssertEqual("doc*with|special<chars>.pdf".sanitizedFileName(), "doc_with_special_chars_.pdf")
#expect("file:with/invalid\\chars?.txt".sanitizedFileName() == "file_with_invalid_chars_.txt")
#expect("doc*with|special<chars>.pdf".sanitizedFileName() == "doc_with_special_chars_.pdf")
// Test spaces are replaced with underscores
XCTAssertEqual("my document.pdf".sanitizedFileName(), "my_document.pdf")
XCTAssertEqual("file with spaces.txt".sanitizedFileName(), "file_with_spaces.txt")
#expect("my document.pdf".sanitizedFileName() == "my_document.pdf")
#expect("file with spaces.txt".sanitizedFileName() == "file_with_spaces.txt")
// Test empty string
XCTAssertEqual("".sanitizedFileName(), "")
#expect("".sanitizedFileName() == "")
}
}