import AVFoundation import Flutter import UIKit import UserNotifications import XCTest @testable import Buzz class RunnerTests: XCTestCase { func testVoiceNotePackagingStagesHaveBoundedDeadlines() { XCTAssertEqual(VoiceNotePackager.videoEnvelopeTimeout, 30) XCTAssertEqual(VoiceNotePackager.exportTimeout, 30) } func testTimedOutVoiceNoteExportCleansLateOutputWithoutRedelivering() throws { let directory = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory( at: directory, withIntermediateDirectories: true ) defer { try? FileManager.default.removeItem(at: directory) } let outputURL = directory.appendingPathComponent("output.mp4") let videoURL = directory.appendingPathComponent("envelope.mp4") try Data([1]).write(to: outputURL) try Data([2]).write(to: videoURL) let completion = VoiceNoteExportCompletion( outputURL: outputURL, videoURL: videoURL ) var cancelCount = 0 var deliveryCount = 0 completion.timeout( cancel: { cancelCount += 1 }, deliver: { deliveryCount += 1 } ) XCTAssertFalse(FileManager.default.fileExists(atPath: outputURL.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: videoURL.path)) // AVFoundation may recreate the destination while cancellation settles. try Data([3]).write(to: outputURL) completion.exportDidFinish( succeeded: false, deliver: { deliveryCount += 1 } ) XCTAssertEqual(cancelCount, 1) XCTAssertEqual(deliveryCount, 1) XCTAssertFalse(FileManager.default.fileExists(atPath: outputURL.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: videoURL.path)) } func testSuccessfulVoiceNoteExportPreservesOutputForFlutter() throws { let directory = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory( at: directory, withIntermediateDirectories: true ) defer { try? FileManager.default.removeItem(at: directory) } let outputURL = directory.appendingPathComponent("output.mp4") let videoURL = directory.appendingPathComponent("envelope.mp4") try Data([1]).write(to: outputURL) try Data([2]).write(to: videoURL) let completion = VoiceNoteExportCompletion( outputURL: outputURL, videoURL: videoURL ) var deliveryCount = 0 completion.exportDidFinish( succeeded: true, deliver: { deliveryCount += 1 } ) XCTAssertEqual(deliveryCount, 1) XCTAssertTrue(FileManager.default.fileExists(atPath: outputURL.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: videoURL.path)) } func testPushAuthorizationStatusNamesCoverDisplayPermissionStates() { XCTAssertEqual(AppDelegate.pushAuthorizationStatusName(.notDetermined), "notDetermined") XCTAssertEqual(AppDelegate.pushAuthorizationStatusName(.denied), "denied") XCTAssertEqual(AppDelegate.pushAuthorizationStatusName(.authorized), "authorized") XCTAssertEqual(AppDelegate.pushAuthorizationStatusName(.provisional), "provisional") XCTAssertEqual(AppDelegate.pushAuthorizationStatusName(.ephemeral), "ephemeral") } func testHuddleActiveTalkerSelectorBoundsAndReactivates() { var selector = HuddleActiveTalkerSelector(capacity: 15) for peer in 0..<15 { XCTAssertEqual( selector.activate(peerIndex: peer, levelDbov: -20), HuddleTalkerSelection(accepted: true, evictedPeerIndex: nil) ) } let rejected = selector.activate(peerIndex: 15, levelDbov: -20) XCTAssertEqual( rejected, HuddleTalkerSelection(accepted: false, evictedPeerIndex: nil) ) var allocationCount = 0 XCTAssertNil(rejected.allocateIfAccepted { allocationCount += 1 return NSObject() }) XCTAssertEqual(allocationCount, 0) XCTAssertEqual(Set(selector.active.keys), Set(0..<15)) selector.remove(7) XCTAssertFalse(selector.active.keys.contains(7)) XCTAssertEqual( selector.activate(peerIndex: 7, levelDbov: -10), HuddleTalkerSelection(accepted: true, evictedPeerIndex: nil) ) XCTAssertTrue(selector.active.keys.contains(7)) } func testHuddleActiveTalkerSelectorUsesRecentActivity() { var selector = HuddleActiveTalkerSelector(capacity: 2) XCTAssertEqual( selector.activate(peerIndex: 4, levelDbov: -10), HuddleTalkerSelection(accepted: true, evictedPeerIndex: nil) ) XCTAssertEqual( selector.activate(peerIndex: 2, levelDbov: -20), HuddleTalkerSelection(accepted: true, evictedPeerIndex: nil) ) XCTAssertEqual( selector.activate(peerIndex: 4, levelDbov: -10), HuddleTalkerSelection(accepted: true, evictedPeerIndex: nil) ) XCTAssertEqual( selector.activate(peerIndex: 9, levelDbov: -5), HuddleTalkerSelection(accepted: true, evictedPeerIndex: 2) ) XCTAssertEqual(Set(selector.active.keys), Set([4, 9])) } func testHuddleActiveTalkerSelectorDoesNotChurnAtEqualLevels() { var selector = HuddleActiveTalkerSelector(capacity: 2) XCTAssertEqual( selector.activate(peerIndex: 4, levelDbov: -127), HuddleTalkerSelection(accepted: true, evictedPeerIndex: nil) ) XCTAssertEqual( selector.activate(peerIndex: 2, levelDbov: -127), HuddleTalkerSelection(accepted: true, evictedPeerIndex: nil) ) let rejected = selector.activate(peerIndex: 9, levelDbov: -127) XCTAssertEqual( rejected, HuddleTalkerSelection(accepted: false, evictedPeerIndex: nil) ) XCTAssertNil(rejected.allocateIfAccepted { NSObject() }) XCTAssertNil(selector.active[9]) XCTAssertEqual(Set(selector.active.keys), Set([2, 4])) } func testHuddleActiveTalkerSelectorExpiresInactiveSlots() { var now: TimeInterval = 0 var selector = HuddleActiveTalkerSelector( capacity: 2, now: { now }, inactivityTimeout: 1 ) _ = selector.activate(peerIndex: 4, levelDbov: -10) now = 0.999 _ = selector.activate(peerIndex: 2, levelDbov: -5) now = 1 XCTAssertEqual( selector.activate(peerIndex: 9, levelDbov: -30), HuddleTalkerSelection(accepted: true, evictedPeerIndex: 4) ) XCTAssertEqual(Set(selector.active.keys), Set([2, 9])) } func testHuddlePacketJitterQueueReordersAndRejectsStaleDuplicates() { var queue = HuddlePacketJitterQueue(capacity: 3, startPackets: 2) queue.enqueue(remotePacket(sequence: 11)) queue.enqueue(remotePacket(sequence: 10)) XCTAssertEqual(queue.packets.map(\.sequence), [10, 11]) XCTAssertEqual(queue.drainOne()?.sequence, 10) queue.enqueue(remotePacket(sequence: 10)) queue.enqueue(remotePacket(sequence: 12)) XCTAssertEqual(queue.packets.map(\.sequence), [11, 12]) } private func remotePacket(sequence: Int) -> HuddleRemoteOpusPacket { HuddleRemoteOpusPacket( peerIndex: 1, sequence: sequence, timestamp48k: Int64(sequence * 960), levelDbov: -20, opus: Data([1]) ) } func testHuddleOpusCodecRoundTripsFixedV2Frame() throws { let encoder = try HuddleOpusEncoder() let decoder = try HuddleOpusDecoder() let samples = (0.. expected ? actual - expected : expected - actual, accuracy ) } } } func testCategoryTrackerHighlightsLastHeaderAtOrAboveTop() { let order = ["people", "nature", "flags"] let offsets: [String: CGFloat] = [ "people": -320, "nature": -12, "flags": 200, ] XCTAssertEqual( NativeEmojiCategoryTracker.selectedSectionID( order: order, offsets: offsets, viewportTop: 0 ), "nature" ) } func testCategoryTrackerFollowsScrollPastEachHeader() { let order = ["people", "nature", "flags"] // Scrolled to the very top: the first section is highlighted. XCTAssertEqual( NativeEmojiCategoryTracker.selectedSectionID( order: order, offsets: ["people": 0, "nature": 400, "flags": 800], viewportTop: 0 ), "people" ) // Scrolled far enough that Flags has reached the top. XCTAssertEqual( NativeEmojiCategoryTracker.selectedSectionID( order: order, offsets: ["people": -800, "nature": -400, "flags": 0], viewportTop: 0 ), "flags" ) } func testCategoryTrackerFallsBackToFirstSectionBeforeAnyHeaderReachesTop() { XCTAssertEqual( NativeEmojiCategoryTracker.selectedSectionID( order: ["people", "nature"], offsets: ["people": 40, "nature": 400], viewportTop: 0 ), "people" ) } func testCategoryTrackerSelectsShortFinalSectionAtClampedBottom() { // The list has overflowed (People scrolled above the top) and its end is on // screen, but the short Custom section's header sits below the top because // the content clamps before it can reach it. The rail must still highlight // Custom rather than leaving Nature — its predecessor — selected. let order = ["people", "nature", "custom"] let offsets: [String: CGFloat] = [ "people": -900, "nature": -420, "custom": 360, ] XCTAssertEqual( NativeEmojiCategoryTracker.selectedSectionID( order: order, offsets: offsets, viewportTop: 0, viewportBottom: 500, contentBottom: 500 ), "custom" ) } func testCategoryTrackerKeepsHeaderRuleWhenContentEndIsOffscreen() { // The same short-final geometry, but the content end is still below the // viewport (the user has not reached the bottom), so the ordinary // header-at-top rule applies and Nature stays selected. let order = ["people", "nature", "custom"] let offsets: [String: CGFloat] = [ "people": -900, "nature": -420, "custom": 360, ] XCTAssertEqual( NativeEmojiCategoryTracker.selectedSectionID( order: order, offsets: offsets, viewportTop: 0, viewportBottom: 500, contentBottom: 900 ), "nature" ) } func testCategoryTrackerDoesNotForceLastSectionForAShortList() { // A list that fits without scrolling has its content end on screen too, but // its first header is still at the top — so the bottom rule must not fire // and steal the highlight to the final section. let order = ["people", "nature"] let offsets: [String: CGFloat] = ["people": 0, "nature": 120] XCTAssertEqual( NativeEmojiCategoryTracker.selectedSectionID( order: order, offsets: offsets, viewportTop: 0, viewportBottom: 500, contentBottom: 240 ), "people" ) } func testCategoryTrackerDoesNotFlickerBackAtPinnedHeaderBoundary() { let order = ["people", "nature", "flags"] // Nature has just become selected. A subsequent layout pass can briefly // report its pinned header a couple of points below the boundary while the // previous header is still pinned. Keep Nature selected through that // transient frame instead of alternating the category rail. XCTAssertEqual( NativeEmojiCategoryTracker.selectedSectionID( order: order, offsets: ["people": 0, "nature": 2, "flags": 400], viewportTop: 0, currentSelection: "nature" ), "nature" ) } func testCategoryTrackerReleasesBoundaryLatchOnRealUpwardScroll() { let order = ["people", "nature", "flags"] XCTAssertEqual( NativeEmojiCategoryTracker.selectedSectionID( order: order, offsets: ["people": 0, "nature": 24, "flags": 424], viewportTop: 0, currentSelection: "nature" ), "people" ) } func testCategoryTrackerReleasesSelectionWhenOldHeaderIsMissing() { XCTAssertEqual( NativeEmojiCategoryTracker.selectedSectionID( order: ["people", "nature", "flags"], offsets: ["people": 0, "flags": 400], viewportTop: 0, currentSelection: "nature" ), "people" ) } func testRemoteEmojiLoaderLimitsConcurrentDownloads() async throws { let maximumConcurrentDownloads = 3 let taskCount = 8 let probe = NativeEmojiDownloadProbe() let tasksAttemptedAdmission = XCTestExpectation( description: "all download tasks attempted admission" ) tasksAttemptedAdmission.expectedFulfillmentCount = taskCount let loader = NativeEmojiRemoteImageLoader( maximumConcurrentDownloads: maximumConcurrentDownloads, cacheByteLimit: 0, admissionAttemptForTesting: { tasksAttemptedAdmission.fulfill() }, downloader: { _ in await probe.holdDownload() return UIImage() } ) let tasks = (0.. UIImage { let colorSpace = try XCTUnwrap(CGColorSpace(name: CGColorSpace.displayP3)) let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) let context = try XCTUnwrap( CGContext( data: nil, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue ) ) context.setFillColor( try XCTUnwrap(CGColor(colorSpace: colorSpace, components: [red, green, blue, 1])) ) context.fill(CGRect(x: 0, y: 0, width: 1, height: 1)) return UIImage(cgImage: try XCTUnwrap(context.makeImage())) } private func sRGBPixel(from image: UIImage) throws -> [UInt8] { let colorSpace = try XCTUnwrap(CGColorSpace(name: CGColorSpace.sRGB)) var bytes = [UInt8](repeating: 0, count: 4) let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) let context = try bytes.withUnsafeMutableBytes { bytes in try XCTUnwrap( CGContext( data: bytes.baseAddress, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue ) ) } context.interpolationQuality = .none context.draw(try XCTUnwrap(image.cgImage), in: CGRect(x: 0, y: 0, width: 1, height: 1)) return bytes } private func fixtureData(named name: String, extension fileExtension: String) throws -> Data { let url = try XCTUnwrap( Bundle(for: RunnerTests.self).url(forResource: name, withExtension: fileExtension)) return try Data(contentsOf: url) } } private final class KeyboardDismissalSpyWindow: UIWindow { private(set) var didForceEndEditing = false override func endEditing(_ force: Bool) -> Bool { didForceEndEditing = force return true } } private enum RelayImagePolicyError: Error { case invalidPng case invalidJpeg case metadataForbidden } private let pngSignature = Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) private let allowedPngAncillaryChunks: Set = [ "cHRM", "gAMA", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "sPLT", "acTL", "fcTL", "fdAT", ] private func assertMatchesRelayImageMetadataPolicy(_ data: Data, mimeType: String) throws { switch mimeType { case "image/png": guard data.count >= pngSignature.count, data.prefix(pngSignature.count) == pngSignature else { throw RelayImagePolicyError.invalidPng } var offset = pngSignature.count while offset < data.count { guard data.count - offset >= 12 else { throw RelayImagePolicyError.invalidPng } let payloadLength = Int(try readUInt32BigEndian(data, at: offset)) guard payloadLength <= data.count - offset - 12 else { throw RelayImagePolicyError.invalidPng } let typeBytes = data[(offset + 4)..<(offset + 8)] guard let type = String(bytes: typeBytes, encoding: .ascii) else { throw RelayImagePolicyError.invalidPng } let chunkEnd = offset + payloadLength + 12 let isAncillary = typeBytes[typeBytes.startIndex] & 0x20 != 0 if isAncillary, !allowedPngAncillaryChunks.contains(type) { throw RelayImagePolicyError.metadataForbidden } offset = chunkEnd if type == "IEND" { guard offset == data.count else { throw RelayImagePolicyError.metadataForbidden } return } } throw RelayImagePolicyError.invalidPng case "image/jpeg": guard data.count >= 2, data[0] == 0xFF, data[1] == 0xD8 else { throw RelayImagePolicyError.invalidJpeg } var offset = 2 var inScan = false while offset < data.count { if inScan, data[offset] != 0xFF { offset += 1 continue } guard data[offset] == 0xFF else { throw RelayImagePolicyError.invalidJpeg } while offset < data.count, data[offset] == 0xFF { offset += 1 } guard offset < data.count else { throw RelayImagePolicyError.invalidJpeg } let marker = data[offset] offset += 1 if inScan, marker == 0x00 { continue } if (0xD0...0xD7).contains(marker) || marker == 0x01 { continue } if marker == 0xD9 { guard offset == data.count else { throw RelayImagePolicyError.metadataForbidden } return } guard marker != 0xD8, data.count - offset >= 2 else { throw RelayImagePolicyError.invalidJpeg } let length = Int(try readUInt16BigEndian(data, at: offset)) guard length >= 2, length <= data.count - offset else { throw RelayImagePolicyError.invalidJpeg } let payload = (offset + 2)..<(offset + length) if marker == 0xE0 { guard payload.count >= 14, data[payload.lowerBound..<(payload.lowerBound + 5)].elementsEqual([ 0x4A, 0x46, 0x49, 0x46, 0x00, ]), payload.count == 14 + 3 * Int(data[payload.lowerBound + 12]) * Int(data[payload.lowerBound + 13]) else { throw RelayImagePolicyError.metadataForbidden } } else if marker == 0xEE { guard payload.count == 12, data[payload.lowerBound..<(payload.lowerBound + 5)].elementsEqual([ 0x41, 0x64, 0x6F, 0x62, 0x65, ]) else { throw RelayImagePolicyError.metadataForbidden } } else if (0xE1...0xED).contains(marker) || marker == 0xEF || marker == 0xFE { throw RelayImagePolicyError.metadataForbidden } offset += length inScan = marker == 0xDA } throw RelayImagePolicyError.invalidJpeg default: XCTFail("Unsupported test MIME type: \(mimeType)") } } private func pngChunkTypes(_ data: Data) throws -> [String] { guard data.count >= pngSignature.count, data.prefix(pngSignature.count) == pngSignature else { throw RelayImagePolicyError.invalidPng } var result: [String] = [] var offset = pngSignature.count while offset < data.count { guard data.count - offset >= 12 else { throw RelayImagePolicyError.invalidPng } let payloadLength = Int(try readUInt32BigEndian(data, at: offset)) guard payloadLength <= data.count - offset - 12 else { throw RelayImagePolicyError.invalidPng } guard let type = String(bytes: data[(offset + 4)..<(offset + 8)], encoding: .ascii) else { throw RelayImagePolicyError.invalidPng } result.append(type) offset += payloadLength + 12 if type == "IEND" { return result } } throw RelayImagePolicyError.invalidPng } private func jpegMetadataMarkers(_ data: Data) throws -> [UInt8] { guard data.count >= 2, data[0] == 0xFF, data[1] == 0xD8 else { throw RelayImagePolicyError.invalidJpeg } var result: [UInt8] = [] var offset = 2 var inScan = false while offset < data.count { if inScan, data[offset] != 0xFF { offset += 1 continue } guard data[offset] == 0xFF else { throw RelayImagePolicyError.invalidJpeg } while offset < data.count, data[offset] == 0xFF { offset += 1 } guard offset < data.count else { throw RelayImagePolicyError.invalidJpeg } let marker = data[offset] offset += 1 if inScan, marker == 0x00 { continue } if (0xD0...0xD7).contains(marker) || marker == 0x01 { continue } if marker == 0xD9 { return result } guard marker != 0xD8, data.count - offset >= 2 else { throw RelayImagePolicyError.invalidJpeg } let length = Int(try readUInt16BigEndian(data, at: offset)) guard length >= 2, length <= data.count - offset else { throw RelayImagePolicyError.invalidJpeg } if (0xE0...0xEF).contains(marker) || marker == 0xFE { result.append(marker) } offset += length inScan = marker == 0xDA } throw RelayImagePolicyError.invalidJpeg } private func readUInt16BigEndian(_ data: Data, at offset: Int) throws -> UInt16 { guard data.count - offset >= 2 else { throw RelayImagePolicyError.invalidJpeg } return UInt16(data[offset]) << 8 | UInt16(data[offset + 1]) } private func readUInt32BigEndian(_ data: Data, at offset: Int) throws -> UInt32 { guard data.count - offset >= 4 else { throw RelayImagePolicyError.invalidPng } return UInt32(data[offset]) << 24 | UInt32(data[offset + 1]) << 16 | UInt32(data[offset + 2]) << 8 | UInt32(data[offset + 3]) } private actor NativeEmojiDownloadProbe { private struct MilestoneWaiter { let count: Int let continuation: CheckedContinuation } private var active = 0 private var peakActive = 0 private var started = 0 private var releaseContinuations: [CheckedContinuation] = [] private var milestoneWaiters: [MilestoneWaiter] = [] func holdDownload() async { active += 1 started += 1 peakActive = max(peakActive, active) resumeReachedMilestones() await withCheckedContinuation { continuation in releaseContinuations.append(continuation) } active -= 1 } func waitUntilStarted(_ count: Int) async { guard started < count else { return } await withCheckedContinuation { continuation in milestoneWaiters.append( MilestoneWaiter(count: count, continuation: continuation) ) } } func releaseOne() { guard !releaseContinuations.isEmpty else { return } releaseContinuations.removeFirst().resume() } func releaseAll() { let continuations = releaseContinuations releaseContinuations.removeAll() for continuation in continuations { continuation.resume() } } func snapshot() -> (active: Int, peakActive: Int, started: Int) { (active, peakActive, started) } private func resumeReachedMilestones() { let reached = milestoneWaiters.filter { $0.count <= started } milestoneWaiters.removeAll { $0.count <= started } for waiter in reached { waiter.continuation.resume() } } }