// // Copyright 2023, 2024 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. // import Combine @testable import ElementX import Foundation import XCTest @MainActor class VoiceMessageMediaManagerTests: XCTestCase { private var voiceMessageMediaManager: VoiceMessageMediaManager! private var voiceMessageCache: VoiceMessageCacheMock! private var mediaProvider: MediaProviderMock! private let someURL = URL.mockMXCAudio private let audioOGGMimeType = "audio/ogg" override func setUp() async throws { voiceMessageCache = VoiceMessageCacheMock() mediaProvider = MediaProviderMock(configuration: .init()) mediaProvider.loadFileFromSourceFilenameClosure = nil mediaProvider.loadFileFromSourceFilenameReturnValue = .failure(.failedRetrievingFile) voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider, voiceMessageCache: voiceMessageCache) } func testLoadVoiceMessageFromSourceUnsupportedMedia() async throws { // Only "audio/ogg" file are supported let unsupportedMediaSource = try MediaSourceProxy(url: someURL, mimeType: "audio/wav") do { _ = try await voiceMessageMediaManager.loadVoiceMessageFromSource(unsupportedMediaSource, body: nil) XCTFail("A `VoiceMessageMediaManagerError.unsupportedMimeTye` error is expected") } catch { switch error as? VoiceMessageMediaManagerError { case .unsupportedMimeTye: break default: XCTFail("A `VoiceMessageMediaManagerError.unsupportedMimeTye` error is expected") } } } func testLoadVoiceMessageFromSourceMimeTypeWithParameters() async throws { // URL representing the file loaded by the media provider let loadedFile = URL("/some/url/loaded_file.ogg") // URL representing the final cached file let cachedConvertedFileURL = URL("/some/url/cached_converted_file.m4a") voiceMessageCache.fileURLForReturnValue = nil let mediaSource = try MediaSourceProxy(url: someURL, mimeType: "audio/ogg; codecs=opus") mediaProvider.loadFileFromSourceFilenameReturnValue = .success(MediaFileHandleProxy.unmanaged(url: loadedFile)) voiceMessageCache.cacheMediaSourceUsingMoveReturnValue = .success(cachedConvertedFileURL) voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider, voiceMessageCache: voiceMessageCache, audioConverter: AudioConverterMock()) do { _ = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) } catch { XCTFail("An unexpected error has occured: \(error)") } } func testLoadVoiceMessageFromSourceAlreadyCached() async throws { // Check if the file is already present in cache voiceMessageCache.fileURLForReturnValue = URL("/converted_file/url") let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) let url = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) XCTAssertEqual(url, URL("/converted_file/url")) // The file must have be search in the cache XCTAssertTrue(voiceMessageCache.fileURLForCalled) XCTAssertEqual(voiceMessageCache.fileURLForReceivedMediaSource, mediaSource) // The file must not have been cached again XCTAssertFalse(voiceMessageCache.cacheMediaSourceUsingMoveCalled) } func testLoadVoiceMessageFromSourceMediaProviderError() async throws { // An error must be reported if the file cannot be retrieved do { voiceMessageCache.fileURLForReturnValue = nil let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) _ = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) XCTFail("A `MediaProviderError.failedRetrievingFile` error is expected") } catch { switch error as? MediaProviderError { case .failedRetrievingFile: break default: XCTFail("A `MediaProviderError.failedRetrievingFile` error is expected") } } } func testLoadVoiceMessageFromSourceSingleCall() async throws { // URL representing the file loaded by the media provider let loadedFile = URL("/some/url/loaded_file") // URL representing the final cached file let cachedConvertedFileURL = URL("/some/url/cached_converted_file") // Check if the file is not already present in cache voiceMessageCache.fileURLForReturnValue = nil let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) mediaProvider.loadFileFromSourceFilenameReturnValue = .success(MediaFileHandleProxy.unmanaged(url: loadedFile)) let audioConverter = AudioConverterMock() voiceMessageCache.cacheMediaSourceUsingMoveReturnValue = .success(cachedConvertedFileURL) voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider, voiceMessageCache: voiceMessageCache, audioConverter: audioConverter) let url = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) // The file must have been converted XCTAssertTrue(audioConverter.convertToMPEG4AACSourceURLDestinationURLCalled) // The converted file must have been cached XCTAssert(voiceMessageCache.cacheMediaSourceUsingMoveCalled) XCTAssertEqual(voiceMessageCache.cacheMediaSourceUsingMoveReceivedArguments?.mediaSource, mediaSource) XCTAssertEqual(voiceMessageCache.cacheMediaSourceUsingMoveReceivedArguments?.fileURL.pathExtension, "m4a") XCTAssertTrue(voiceMessageCache.cacheMediaSourceUsingMoveReceivedArguments?.move ?? false) // The returned URL must point to the cached converted file XCTAssertEqual(url, cachedConvertedFileURL) } func testLoadVoiceMessageFromSourceMultipleCalls() async throws { // URL representing the file loaded by the media provider let loadedFile = URL("/some/url/loaded_file") // URL representing the final cached file let cachedConvertedFileURL = URL("/some/url/cached_converted_file") // Multiple calls var cachedURL: URL? voiceMessageCache.fileURLForClosure = { _ in cachedURL } voiceMessageCache.cacheMediaSourceUsingMoveClosure = { _, _, _ in cachedURL = cachedConvertedFileURL return .success(cachedConvertedFileURL) } let audioConverter = AudioConverterMock() mediaProvider.loadFileFromSourceFilenameReturnValue = .success(MediaFileHandleProxy.unmanaged(url: loadedFile)) voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider, voiceMessageCache: voiceMessageCache, audioConverter: audioConverter) let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) for _ in 0..<10 { let url = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) XCTAssertEqual(url, cachedConvertedFileURL) } // The file must have been converted only once XCTAssertEqual(audioConverter.convertToMPEG4AACSourceURLDestinationURLCallsCount, 1) // The converted file must have been cached only once XCTAssertEqual(voiceMessageCache.cacheMediaSourceUsingMoveCallsCount, 1) } }