From ae7f7b3f7a6ea30d021d4ea44da8be942efbf6a2 Mon Sep 17 00:00:00 2001 From: Oliver Drobnik Date: Sat, 7 Jun 2025 20:51:21 +0200 Subject: [PATCH] Modernized unit tests --- .../SwiftIMAPTests/QuotedPrintableTests.swift | 74 +++++-- .../String+EmailTests.swift | 183 ++++++++++++++---- .../String+HostnameTests.swift | 25 ++- .../SwiftMailCoreTests/String+MIMETests.swift | 117 ++++++++--- .../String+UtilitiesTests.swift | 23 ++- 5 files changed, 318 insertions(+), 104 deletions(-) diff --git a/Tests/SwiftIMAPTests/QuotedPrintableTests.swift b/Tests/SwiftIMAPTests/QuotedPrintableTests.swift index b7f343e..abf1af4 100644 --- a/Tests/SwiftIMAPTests/QuotedPrintableTests.swift +++ b/Tests/SwiftIMAPTests/QuotedPrintableTests.swift @@ -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 diff --git a/Tests/SwiftMailCoreTests/String+EmailTests.swift b/Tests/SwiftMailCoreTests/String+EmailTests.swift index aacb6e7..4c0e28b 100644 --- a/Tests/SwiftMailCoreTests/String+EmailTests.swift +++ b/Tests/SwiftMailCoreTests/String+EmailTests.swift @@ -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@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") } } \ No newline at end of file diff --git a/Tests/SwiftMailCoreTests/String+HostnameTests.swift b/Tests/SwiftMailCoreTests/String+HostnameTests.swift index 245db0d..73fb2a0 100644 --- a/Tests/SwiftMailCoreTests/String+HostnameTests.swift +++ b/Tests/SwiftMailCoreTests/String+HostnameTests.swift @@ -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 } diff --git a/Tests/SwiftMailCoreTests/String+MIMETests.swift b/Tests/SwiftMailCoreTests/String+MIMETests.swift index 0397ba3..a52dd55 100644 --- a/Tests/SwiftMailCoreTests/String+MIMETests.swift +++ b/Tests/SwiftMailCoreTests/String+MIMETests.swift @@ -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") } } \ No newline at end of file diff --git a/Tests/SwiftMailCoreTests/String+UtilitiesTests.swift b/Tests/SwiftMailCoreTests/String+UtilitiesTests.swift index 64bcf43..7cd463d 100644 --- a/Tests/SwiftMailCoreTests/String+UtilitiesTests.swift +++ b/Tests/SwiftMailCoreTests/String+UtilitiesTests.swift @@ -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.pdf".sanitizedFileName(), "doc_with_special_chars_.pdf") + #expect("file:with/invalid\\chars?.txt".sanitizedFileName() == "file_with_invalid_chars_.txt") + #expect("doc*with|special.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() == "") } } \ No newline at end of file