454 lines
17 KiB
Swift
454 lines
17 KiB
Swift
import XCTest
|
|
@testable import Rosetta
|
|
|
|
/// Comprehensive tests for file attachment download, caching, preview parsing,
|
|
/// icon mapping, and cross-platform parity.
|
|
final class FileAttachmentTests: XCTestCase {
|
|
|
|
// =========================================================================
|
|
// MARK: - AttachmentPreviewCodec: File Preview Parsing
|
|
// =========================================================================
|
|
|
|
func testParseFilePreview_TagSizeFilename() {
|
|
let preview = "4e6712a0-31c0-4b0d-b17c-83170709ec02::12345::document.pdf"
|
|
let parsed = AttachmentPreviewCodec.parseFilePreview(preview)
|
|
XCTAssertEqual(parsed.downloadTag, "4e6712a0-31c0-4b0d-b17c-83170709ec02")
|
|
XCTAssertEqual(parsed.fileSize, 12345)
|
|
XCTAssertEqual(parsed.fileName, "document.pdf")
|
|
}
|
|
|
|
func testParseFilePreview_NoTag() {
|
|
let preview = "12345::document.pdf"
|
|
let parsed = AttachmentPreviewCodec.parseFilePreview(preview)
|
|
XCTAssertEqual(parsed.downloadTag, "")
|
|
XCTAssertEqual(parsed.fileSize, 12345)
|
|
XCTAssertEqual(parsed.fileName, "document.pdf")
|
|
}
|
|
|
|
func testParseFilePreview_PlaceholderBeforeUpload() {
|
|
let preview = "::500::notes.txt"
|
|
let parsed = AttachmentPreviewCodec.parseFilePreview(preview)
|
|
XCTAssertEqual(parsed.downloadTag, "")
|
|
XCTAssertEqual(parsed.fileSize, 500)
|
|
XCTAssertEqual(parsed.fileName, "notes.txt")
|
|
}
|
|
|
|
func testParseFilePreview_EmptyPreview() {
|
|
let parsed = AttachmentPreviewCodec.parseFilePreview("")
|
|
XCTAssertEqual(parsed.downloadTag, "")
|
|
XCTAssertEqual(parsed.fileName, "file") // fallback
|
|
XCTAssertEqual(parsed.fileSize, 0)
|
|
}
|
|
|
|
func testParseFilePreview_CustomFallbacks() {
|
|
let parsed = AttachmentPreviewCodec.parseFilePreview(
|
|
"", fallbackFileName: "unknown.bin", fallbackFileSize: 999
|
|
)
|
|
XCTAssertEqual(parsed.fileName, "unknown.bin")
|
|
XCTAssertEqual(parsed.fileSize, 999)
|
|
}
|
|
|
|
func testParseFilePreview_FilenameWithColons() {
|
|
// Edge case: filename containing "::" (unlikely but possible)
|
|
let preview = "4e6712a0-31c0-4b0d-b17c-83170709ec02::1024::file::with::colons.txt"
|
|
let parsed = AttachmentPreviewCodec.parseFilePreview(preview)
|
|
XCTAssertEqual(parsed.downloadTag, "4e6712a0-31c0-4b0d-b17c-83170709ec02")
|
|
XCTAssertEqual(parsed.fileSize, 1024)
|
|
XCTAssertEqual(parsed.fileName, "file::with::colons.txt")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - AttachmentPreviewCodec: Download Tag Extraction
|
|
// =========================================================================
|
|
|
|
func testDownloadTag_ValidUUID() {
|
|
let preview = "4e6712a0-31c0-4b0d-b17c-83170709ec02::blurhash"
|
|
XCTAssertEqual(
|
|
AttachmentPreviewCodec.downloadTag(from: preview),
|
|
"4e6712a0-31c0-4b0d-b17c-83170709ec02"
|
|
)
|
|
}
|
|
|
|
func testDownloadTag_NoTag() {
|
|
let preview = "::blurhash"
|
|
XCTAssertEqual(AttachmentPreviewCodec.downloadTag(from: preview), "")
|
|
}
|
|
|
|
func testDownloadTag_EmptyPreview() {
|
|
XCTAssertEqual(AttachmentPreviewCodec.downloadTag(from: ""), "")
|
|
}
|
|
|
|
func testDownloadTag_JustBlurhash() {
|
|
let preview = "LVRv{GtRSXWB"
|
|
XCTAssertEqual(AttachmentPreviewCodec.downloadTag(from: preview), "")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - AttachmentPreviewCodec: BlurHash Extraction
|
|
// =========================================================================
|
|
|
|
func testBlurHash_TagAndHash() {
|
|
let preview = "4e6712a0-31c0-4b0d-b17c-83170709ec02::LVRv{GtR"
|
|
XCTAssertEqual(AttachmentPreviewCodec.blurHash(from: preview), "LVRv{GtR")
|
|
}
|
|
|
|
func testBlurHash_WithDimensionSuffix() {
|
|
let preview = "4e6712a0-31c0-4b0d-b17c-83170709ec02::LVRv{GtR|640x480"
|
|
XCTAssertEqual(AttachmentPreviewCodec.blurHash(from: preview), "LVRv{GtR")
|
|
}
|
|
|
|
func testBlurHash_PlainHash() {
|
|
let preview = "LVRv{GtRSXWB"
|
|
XCTAssertEqual(AttachmentPreviewCodec.blurHash(from: preview), "LVRv{GtRSXWB")
|
|
}
|
|
|
|
func testBlurHash_PlaceholderFormat() {
|
|
let preview = "::LVRv{GtR"
|
|
XCTAssertEqual(AttachmentPreviewCodec.blurHash(from: preview), "LVRv{GtR")
|
|
}
|
|
|
|
func testBlurHash_LegacyNonUUIDTagPrefix() {
|
|
let preview = "jbov1nac::LVRv{GtRSXWB"
|
|
XCTAssertEqual(AttachmentPreviewCodec.blurHash(from: preview), "LVRv{GtRSXWB")
|
|
}
|
|
|
|
func testBlurHash_LegacyNonUUIDTagWithDimensions() {
|
|
let preview = "jbov1nac::LVRv{GtR|640x480"
|
|
XCTAssertEqual(AttachmentPreviewCodec.blurHash(from: preview), "LVRv{GtR")
|
|
}
|
|
|
|
func testBlurHash_DoesNotStripUnknownNonUUIDPrefix() {
|
|
let preview = "legacy_upload_id::LVRv{GtRSXWB"
|
|
XCTAssertEqual(AttachmentPreviewCodec.blurHash(from: preview), "legacy_upload_id::LVRv{GtRSXWB")
|
|
}
|
|
|
|
func testBlurHash_LegacyNonUUIDTagWithEmptySuffix() {
|
|
let preview = "jbov1nac::"
|
|
XCTAssertEqual(AttachmentPreviewCodec.blurHash(from: preview), "")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - AttachmentPreviewCodec: Image Dimensions
|
|
// =========================================================================
|
|
|
|
func testImageDimensions_Present() {
|
|
let preview = "tag::LVRv{GtR|640x480"
|
|
let dims = AttachmentPreviewCodec.imageDimensions(from: preview)
|
|
XCTAssertEqual(dims?.width, 640)
|
|
XCTAssertEqual(dims?.height, 480)
|
|
}
|
|
|
|
func testImageDimensions_Missing() {
|
|
let preview = "tag::LVRv{GtR"
|
|
XCTAssertNil(AttachmentPreviewCodec.imageDimensions(from: preview))
|
|
}
|
|
|
|
func testImageDimensions_TooSmall() {
|
|
let preview = "tag::LVRv{GtR|5x5"
|
|
XCTAssertNil(AttachmentPreviewCodec.imageDimensions(from: preview))
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - AttachmentPreviewCodec: Call Duration
|
|
// =========================================================================
|
|
|
|
func testCallDuration_PlainNumber() {
|
|
XCTAssertEqual(AttachmentPreviewCodec.parseCallDurationSeconds("45"), 45)
|
|
}
|
|
|
|
func testCallDuration_Zero() {
|
|
XCTAssertEqual(AttachmentPreviewCodec.parseCallDurationSeconds("0"), 0)
|
|
}
|
|
|
|
func testCallDuration_Empty() {
|
|
XCTAssertEqual(AttachmentPreviewCodec.parseCallDurationSeconds(""), 0)
|
|
}
|
|
|
|
func testCallDuration_WithTag() {
|
|
let preview = "4e6712a0-31c0-4b0d-b17c-83170709ec02::60"
|
|
XCTAssertEqual(AttachmentPreviewCodec.parseCallDurationSeconds(preview), 60)
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - AttachmentPreviewCodec: Compose
|
|
// =========================================================================
|
|
|
|
func testCompose_TagAndPayload() {
|
|
let result = AttachmentPreviewCodec.compose(
|
|
downloadTag: "4e6712a0-31c0-4b0d-b17c-83170709ec02",
|
|
payload: "LVRv{GtR"
|
|
)
|
|
XCTAssertEqual(result, "4e6712a0-31c0-4b0d-b17c-83170709ec02::LVRv{GtR")
|
|
}
|
|
|
|
func testCompose_EmptyTag() {
|
|
XCTAssertEqual(AttachmentPreviewCodec.compose(downloadTag: "", payload: "LVRv"), "LVRv")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - MessageAttachment: effectiveDownloadTag
|
|
// =========================================================================
|
|
|
|
func testEffectiveTag_TransportTagPresent() {
|
|
let att = MessageAttachment(
|
|
id: "test", preview: "old-tag::hash", transportTag: "new-tag"
|
|
)
|
|
XCTAssertEqual(att.effectiveDownloadTag, "new-tag")
|
|
}
|
|
|
|
func testEffectiveTag_FallbackToPreview() {
|
|
let att = MessageAttachment(
|
|
id: "test",
|
|
preview: "4e6712a0-31c0-4b0d-b17c-83170709ec02::hash"
|
|
)
|
|
XCTAssertEqual(att.effectiveDownloadTag, "4e6712a0-31c0-4b0d-b17c-83170709ec02")
|
|
}
|
|
|
|
func testEffectiveTag_BothEmpty() {
|
|
let att = MessageAttachment(id: "test", preview: "::hash")
|
|
XCTAssertEqual(att.effectiveDownloadTag, "")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - File Icon Mapping (Telegram parity)
|
|
// =========================================================================
|
|
|
|
/// Tests the file icon mapping used by MessageFileView (SwiftUI) and
|
|
/// NativeMessageCell.fileIcon(for:) (UIKit). Both must return same values.
|
|
private func fileIcon(for fileName: String) -> String {
|
|
let ext = (fileName as NSString).pathExtension.lowercased()
|
|
switch ext {
|
|
case "pdf": return "doc.fill"
|
|
case "zip", "rar", "7z": return "doc.zipper"
|
|
case "jpg", "jpeg", "png", "gif": return "photo.fill"
|
|
case "mp4", "mov", "avi": return "film.fill"
|
|
case "mp3", "wav", "aac": return "waveform"
|
|
default: return "doc.fill"
|
|
}
|
|
}
|
|
|
|
func testFileIcon_PDF() {
|
|
XCTAssertEqual(fileIcon(for: "report.pdf"), "doc.fill")
|
|
}
|
|
|
|
func testFileIcon_ZIP() {
|
|
XCTAssertEqual(fileIcon(for: "archive.zip"), "doc.zipper")
|
|
}
|
|
|
|
func testFileIcon_RAR() {
|
|
XCTAssertEqual(fileIcon(for: "data.rar"), "doc.zipper")
|
|
}
|
|
|
|
func testFileIcon_7Z() {
|
|
XCTAssertEqual(fileIcon(for: "backup.7z"), "doc.zipper")
|
|
}
|
|
|
|
func testFileIcon_JPG() {
|
|
XCTAssertEqual(fileIcon(for: "photo.jpg"), "photo.fill")
|
|
}
|
|
|
|
func testFileIcon_JPEG() {
|
|
XCTAssertEqual(fileIcon(for: "photo.jpeg"), "photo.fill")
|
|
}
|
|
|
|
func testFileIcon_PNG() {
|
|
XCTAssertEqual(fileIcon(for: "screenshot.png"), "photo.fill")
|
|
}
|
|
|
|
func testFileIcon_GIF() {
|
|
XCTAssertEqual(fileIcon(for: "animation.gif"), "photo.fill")
|
|
}
|
|
|
|
func testFileIcon_MP4() {
|
|
XCTAssertEqual(fileIcon(for: "video.mp4"), "film.fill")
|
|
}
|
|
|
|
func testFileIcon_MOV() {
|
|
XCTAssertEqual(fileIcon(for: "clip.mov"), "film.fill")
|
|
}
|
|
|
|
func testFileIcon_AVI() {
|
|
XCTAssertEqual(fileIcon(for: "movie.avi"), "film.fill")
|
|
}
|
|
|
|
func testFileIcon_MP3() {
|
|
XCTAssertEqual(fileIcon(for: "song.mp3"), "waveform")
|
|
}
|
|
|
|
func testFileIcon_WAV() {
|
|
XCTAssertEqual(fileIcon(for: "audio.wav"), "waveform")
|
|
}
|
|
|
|
func testFileIcon_AAC() {
|
|
XCTAssertEqual(fileIcon(for: "voice.aac"), "waveform")
|
|
}
|
|
|
|
func testFileIcon_Unknown() {
|
|
XCTAssertEqual(fileIcon(for: "data.bin"), "doc.fill")
|
|
}
|
|
|
|
func testFileIcon_NoExtension() {
|
|
XCTAssertEqual(fileIcon(for: "Makefile"), "doc.fill")
|
|
}
|
|
|
|
func testFileIcon_UpperCase() {
|
|
XCTAssertEqual(fileIcon(for: "IMAGE.PNG"), "photo.fill")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - AttachmentCache: File Save/Load Round-Trip
|
|
// =========================================================================
|
|
|
|
func testFileSaveAndLoad_Plaintext() {
|
|
// Without private key, files are saved as plaintext
|
|
let cache = AttachmentCache.shared
|
|
let originalKey = cache.privateKey
|
|
cache.privateKey = nil
|
|
defer { cache.privateKey = originalKey }
|
|
|
|
let testData = "Hello, world!".data(using: .utf8)!
|
|
let attId = "test_plain_\(UUID().uuidString.prefix(8))"
|
|
let fileName = "test.txt"
|
|
|
|
let url = cache.saveFile(testData, forAttachmentId: attId, fileName: fileName)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
|
|
|
|
// fileURL should find it
|
|
let foundURL = cache.fileURL(forAttachmentId: attId, fileName: fileName)
|
|
XCTAssertNotNil(foundURL)
|
|
|
|
// loadFileData should return original data
|
|
let loaded = cache.loadFileData(forAttachmentId: attId, fileName: fileName)
|
|
XCTAssertEqual(loaded, testData)
|
|
|
|
// Cleanup
|
|
try? FileManager.default.removeItem(at: url)
|
|
}
|
|
|
|
func testFileSaveAndLoad_Encrypted() throws {
|
|
let cache = AttachmentCache.shared
|
|
let originalKey = cache.privateKey
|
|
|
|
// Use a test key
|
|
cache.privateKey = "test_private_key_for_encryption"
|
|
defer { cache.privateKey = originalKey }
|
|
|
|
let testData = "Encrypted file content 🔒".data(using: .utf8)!
|
|
let attId = "test_enc_\(UUID().uuidString.prefix(8))"
|
|
let fileName = "secret.pdf"
|
|
|
|
let url = cache.saveFile(testData, forAttachmentId: attId, fileName: fileName)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
|
|
XCTAssertTrue(url.lastPathComponent.hasSuffix(".enc"))
|
|
|
|
// Raw file should NOT be the original data (it's encrypted)
|
|
let rawData = try Data(contentsOf: url)
|
|
XCTAssertNotEqual(rawData, testData)
|
|
|
|
// loadFileData should decrypt and return original
|
|
let loaded = cache.loadFileData(forAttachmentId: attId, fileName: fileName)
|
|
XCTAssertEqual(loaded, testData)
|
|
|
|
// Cleanup
|
|
try? FileManager.default.removeItem(at: url)
|
|
}
|
|
|
|
func testFileURL_NotCached() {
|
|
let url = AttachmentCache.shared.fileURL(
|
|
forAttachmentId: "nonexistent_\(UUID())", fileName: "nope.txt"
|
|
)
|
|
XCTAssertNil(url)
|
|
}
|
|
|
|
func testLoadFileData_NotCached() {
|
|
let data = AttachmentCache.shared.loadFileData(
|
|
forAttachmentId: "nonexistent_\(UUID())", fileName: "nope.txt"
|
|
)
|
|
XCTAssertNil(data)
|
|
}
|
|
|
|
func testFileSave_SlashInFilename() {
|
|
let cache = AttachmentCache.shared
|
|
let originalKey = cache.privateKey
|
|
cache.privateKey = nil
|
|
defer { cache.privateKey = originalKey }
|
|
|
|
let testData = "data".data(using: .utf8)!
|
|
let attId = "test_slash_\(UUID().uuidString.prefix(8))"
|
|
// Slash should be replaced with underscore
|
|
let fileName = "path/to/file.txt"
|
|
|
|
let url = cache.saveFile(testData, forAttachmentId: attId, fileName: fileName)
|
|
XCTAssertFalse(url.lastPathComponent.contains("/"))
|
|
|
|
let loaded = cache.loadFileData(forAttachmentId: attId, fileName: fileName)
|
|
XCTAssertEqual(loaded, testData)
|
|
|
|
try? FileManager.default.removeItem(at: url)
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - AttachmentType JSON Encode/Decode (cross-platform parity)
|
|
// =========================================================================
|
|
|
|
func testAttachmentType_EncodeValues() throws {
|
|
// Verify wire format matches Desktop/Android (0,1,2,3,4)
|
|
let encoder = JSONEncoder()
|
|
let types: [AttachmentType] = [.image, .messages, .file, .avatar, .call]
|
|
for (idx, type) in types.enumerated() {
|
|
let data = try encoder.encode(type)
|
|
let str = String(data: data, encoding: .utf8)!
|
|
XCTAssertEqual(str, "\(idx)", "AttachmentType.\(type) should encode as \(idx)")
|
|
}
|
|
}
|
|
|
|
func testMessageAttachment_BackwardCompatDecode() throws {
|
|
// Old DB JSON without transportTag/transportServer should decode fine
|
|
let json = #"{"id":"abc","preview":"tag::hash","blob":"data","type":0}"#
|
|
let att = try JSONDecoder().decode(MessageAttachment.self, from: Data(json.utf8))
|
|
XCTAssertEqual(att.id, "abc")
|
|
XCTAssertEqual(att.type, .image)
|
|
XCTAssertEqual(att.transportTag, "") // default
|
|
XCTAssertEqual(att.transportServer, "") // default
|
|
}
|
|
|
|
func testMessageAttachment_NewFieldsDecode() throws {
|
|
let json = #"{"id":"abc","preview":"hash","blob":"","type":3,"transportTag":"cdn-tag","transportServer":"https://cdn.example.com"}"#
|
|
let att = try JSONDecoder().decode(MessageAttachment.self, from: Data(json.utf8))
|
|
XCTAssertEqual(att.type, .avatar)
|
|
XCTAssertEqual(att.transportTag, "cdn-tag")
|
|
XCTAssertEqual(att.transportServer, "https://cdn.example.com")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - File Size Formatting
|
|
// =========================================================================
|
|
|
|
private func formattedFileSize(bytes: Int) -> String {
|
|
if bytes < 1024 { return "\(bytes) B" }
|
|
if bytes < 1024 * 1024 { return String(format: "%.1f KB", Double(bytes) / 1024) }
|
|
if bytes < 1024 * 1024 * 1024 { return String(format: "%.1f MB", Double(bytes) / (1024 * 1024)) }
|
|
return String(format: "%.1f GB", Double(bytes) / (1024 * 1024 * 1024))
|
|
}
|
|
|
|
func testFileSize_Bytes() {
|
|
XCTAssertEqual(formattedFileSize(bytes: 500), "500 B")
|
|
}
|
|
|
|
func testFileSize_KB() {
|
|
XCTAssertEqual(formattedFileSize(bytes: 2048), "2.0 KB")
|
|
}
|
|
|
|
func testFileSize_MB() {
|
|
XCTAssertEqual(formattedFileSize(bytes: 5_242_880), "5.0 MB")
|
|
}
|
|
|
|
func testFileSize_GB() {
|
|
XCTAssertEqual(formattedFileSize(bytes: 1_073_741_824), "1.0 GB")
|
|
}
|
|
|
|
func testFileSize_Zero() {
|
|
XCTAssertEqual(formattedFileSize(bytes: 0), "0 B")
|
|
}
|
|
}
|