import BitFoundation import CoreGraphics import Foundation import ImageIO import Testing import UniformTypeIdentifiers #if os(iOS) import UIKit #else import AppKit #endif @testable import bitchat struct ChatMediaPreparationTests { @Test func prepareVoiceNotePacket_buildsEncodedAudioPacket() throws { let url = FileManager.default.temporaryDirectory.appendingPathComponent("voice-\(UUID().uuidString).m4a") try Data("voice".utf8).write(to: url, options: .atomic) defer { try? FileManager.default.removeItem(at: url) } let packet = try ChatMediaPreparation.prepareVoiceNotePacket(at: url) #expect(packet.fileName == url.lastPathComponent) #expect(packet.mimeType == "audio/mp4") #expect(packet.fileSize == 5) #expect(packet.encode() != nil) } @Test func prepareVoiceNotePacket_rejectsOversizedAudio() throws { let url = FileManager.default.temporaryDirectory.appendingPathComponent("voice-too-large-\(UUID().uuidString).m4a") try Data(repeating: 0x55, count: FileTransferLimits.maxVoiceNoteBytes + 1).write(to: url, options: .atomic) defer { try? FileManager.default.removeItem(at: url) } #expect(throws: ChatMediaPreparationError.voiceNoteTooLarge(bytes: FileTransferLimits.maxVoiceNoteBytes + 1)) { try ChatMediaPreparation.prepareVoiceNotePacket(at: url) } } @Test func prepareImagePacket_rejectsInvalidImage() throws { let url = FileManager.default.temporaryDirectory.appendingPathComponent("invalid-\(UUID().uuidString).jpg") try Data("not-an-image".utf8).write(to: url, options: .atomic) defer { try? FileManager.default.removeItem(at: url) } #expect(throws: ImageUtilsError.invalidImage) { try ChatMediaPreparation.prepareImagePacket(from: url) } } @Test func prepareImagePacket_buildsEncodedJpegPacket() throws { let sourceURL = try makeTemporaryImageURL() defer { try? FileManager.default.removeItem(at: sourceURL) } let prepared = try ChatMediaPreparation.prepareImagePacket(from: sourceURL) defer { try? FileManager.default.removeItem(at: prepared.outputURL) } #expect(prepared.packet.fileName == prepared.outputURL.lastPathComponent) #expect(prepared.packet.mimeType == "image/jpeg") #expect(prepared.packet.fileSize == UInt64(prepared.packet.content.count)) #expect(prepared.packet.encode() != nil) } /// A genuinely detailed photo must prepare to more than /// `TransportConfig.wifiBulkMinPayloadBytes` (64 KiB); otherwise the Wi-Fi /// bulk (AWDL) data plane is never offered in production because /// `WifiBulkPolicy.shouldOffer` requires `payloadBytes > 64 KiB`. /// Regression guard for the ~40 KB over-compression gap (PR #1385). @Test func prepareImagePacket_detailedImageExceedsWifiBulkThreshold() throws { let sourceURL = try makeDetailedImageURL(dimension: 1200) defer { try? FileManager.default.removeItem(at: sourceURL) } let prepared = try ChatMediaPreparation.prepareImagePacket(from: sourceURL) defer { try? FileManager.default.removeItem(at: prepared.outputURL) } // Comfortably above the 64 KiB Wi-Fi bulk offer threshold... #expect(prepared.packet.content.count > TransportConfig.wifiBulkMinPayloadBytes) // ...and still within the hard image cap for the BLE path. #expect(prepared.packet.content.count <= FileTransferLimits.maxImageBytes) } } private func makeTemporaryImageURL() throws -> URL { let url = FileManager.default.temporaryDirectory.appendingPathComponent("image-\(UUID().uuidString).png") #if os(iOS) let renderer = UIGraphicsImageRenderer(size: CGSize(width: 64, height: 64)) let image = renderer.image { context in UIColor.systemTeal.setFill() context.fill(CGRect(x: 0, y: 0, width: 64, height: 64)) } guard let data = image.pngData() else { throw ChatMediaPreparationTestError.imageEncodingFailed } #else let image = NSImage(size: NSSize(width: 64, height: 64)) image.lockFocus() NSColor.systemTeal.setFill() NSRect(x: 0, y: 0, width: 64, height: 64).fill() image.unlockFocus() guard let tiff = image.tiffRepresentation, let bitmap = NSBitmapImageRep(data: tiff), let data = bitmap.representation(using: .png, properties: [:]) else { throw ChatMediaPreparationTestError.imageEncodingFailed } #endif try data.write(to: url, options: .atomic) return url } /// Builds a random-noise PNG. Noise is incompressible, so the JPEG the prep /// pipeline produces stays large — a faithful stand-in for a detailed photo, /// unlike a flat solid-color image which would compress to a few KB regardless /// of dimension. private func makeDetailedImageURL(dimension: Int) throws -> URL { let width = dimension let height = dimension let bytesPerPixel = 4 let bytesPerRow = width * bytesPerPixel var pixels = [UInt8](repeating: 0, count: bytesPerRow * height) for index in pixels.indices { pixels[index] = UInt8.random(in: 0...255) } let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB() guard let context = CGContext( data: &pixels, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ), let cgImage = context.makeImage() else { throw ChatMediaPreparationTestError.imageEncodingFailed } let url = FileManager.default.temporaryDirectory.appendingPathComponent("detailed-\(UUID().uuidString).png") guard let destination = CGImageDestinationCreateWithURL( url as CFURL, UTType.png.identifier as CFString, 1, nil ) else { throw ChatMediaPreparationTestError.imageEncodingFailed } CGImageDestinationAddImage(destination, cgImage, nil) guard CGImageDestinationFinalize(destination) else { throw ChatMediaPreparationTestError.imageEncodingFailed } return url } private enum ChatMediaPreparationTestError: Error { case imageEncodingFailed }