diff --git a/Sources/StreamVideo/WebRTC/v2/VideoCapturing/StreamCaptureDeviceProvider.swift b/Sources/StreamVideo/WebRTC/v2/VideoCapturing/StreamCaptureDeviceProvider.swift index efc21642c..fe3e42062 100644 --- a/Sources/StreamVideo/WebRTC/v2/VideoCapturing/StreamCaptureDeviceProvider.swift +++ b/Sources/StreamVideo/WebRTC/v2/VideoCapturing/StreamCaptureDeviceProvider.swift @@ -7,20 +7,20 @@ import StreamWebRTC final class StreamCaptureDeviceProvider { - private let firstResultIfMiss: Bool + private let useFallback: Bool private var devices: [AVCaptureDevice] { RTCCameraVideoCapturer.captureDevices() } - init(firstResultIfMiss: Bool = true) { - self.firstResultIfMiss = firstResultIfMiss + init(useFallback: Bool = true) { + self.useFallback = useFallback } func device(for position: AVCaptureDevice.Position) -> AVCaptureDevice? { if let deviceFound = devices.first(where: { $0.position == position }) { return deviceFound - } else if firstResultIfMiss { + } else if useFallback { return devices.first } else { return nil diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index df3398d09..a01bf068c 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -449,6 +449,9 @@ 40B48C402D14DB26002C4EAB /* MediaTransceiverStorage_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B48C3F2D14DB26002C4EAB /* MediaTransceiverStorage_Tests.swift */; }; 40B48C472D14E803002C4EAB /* StreamVideoCapturing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B48C462D14E803002C4EAB /* StreamVideoCapturing.swift */; }; 40B48C492D14E822002C4EAB /* MockStreamVideoCapturer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B48C482D14E822002C4EAB /* MockStreamVideoCapturer.swift */; }; + 40B48C4C2D14F721002C4EAB /* RTPMapVisitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B48C4B2D14F721002C4EAB /* RTPMapVisitorTests.swift */; }; + 40B48C4F2D14F77B002C4EAB /* SupportedPrefix_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B48C4E2D14F77B002C4EAB /* SupportedPrefix_Tests.swift */; }; + 40B48C512D14F7AE002C4EAB /* SDPParser_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B48C502D14F7AE002C4EAB /* SDPParser_Tests.swift */; }; 40B499CA2AC1A5E100A53B60 /* OSLogDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B499C92AC1A5E100A53B60 /* OSLogDestination.swift */; }; 40B499CC2AC1A90F00A53B60 /* DeeplinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B499CB2AC1A90F00A53B60 /* DeeplinkTests.swift */; }; 40B499CE2AC1AA0900A53B60 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4030E59F2A9DF5BD003E8CBA /* AppEnvironment.swift */; }; @@ -1803,6 +1806,9 @@ 40B48C3F2D14DB26002C4EAB /* MediaTransceiverStorage_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTransceiverStorage_Tests.swift; sourceTree = ""; }; 40B48C462D14E803002C4EAB /* StreamVideoCapturing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamVideoCapturing.swift; sourceTree = ""; }; 40B48C482D14E822002C4EAB /* MockStreamVideoCapturer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStreamVideoCapturer.swift; sourceTree = ""; }; + 40B48C4B2D14F721002C4EAB /* RTPMapVisitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTPMapVisitorTests.swift; sourceTree = ""; }; + 40B48C4E2D14F77B002C4EAB /* SupportedPrefix_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedPrefix_Tests.swift; sourceTree = ""; }; + 40B48C502D14F7AE002C4EAB /* SDPParser_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDPParser_Tests.swift; sourceTree = ""; }; 40B499C92AC1A5E100A53B60 /* OSLogDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogDestination.swift; sourceTree = ""; }; 40B499CB2AC1A90F00A53B60 /* DeeplinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkTests.swift; sourceTree = ""; }; 40BBC4762C6227D5002AEF92 /* DemoTranscriptionButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoTranscriptionButtonView.swift; sourceTree = ""; }; @@ -3367,6 +3373,7 @@ 406B3C072C8F602D00FC93A1 /* v2 */ = { isa = PBXGroup; children = ( + 40B48C4A2D14F718002C4EAB /* SDP Parsing */, 4029E9582CB9445D00E1D571 /* Extensions */, 408CF9C12CAE885800F56833 /* IntegrationTests */, 40C9E4632C99886200802B28 /* WebRTCCoorindator_Tests.swift */, @@ -3894,6 +3901,24 @@ path = Utilities; sourceTree = ""; }; + 40B48C4A2D14F718002C4EAB /* SDP Parsing */ = { + isa = PBXGroup; + children = ( + 40B48C4D2D14F76A002C4EAB /* Visitors */, + 40B48C502D14F7AE002C4EAB /* SDPParser_Tests.swift */, + 40B48C4E2D14F77B002C4EAB /* SupportedPrefix_Tests.swift */, + ); + path = "SDP Parsing"; + sourceTree = ""; + }; + 40B48C4D2D14F76A002C4EAB /* Visitors */ = { + isa = PBXGroup; + children = ( + 40B48C4B2D14F721002C4EAB /* RTPMapVisitorTests.swift */, + ); + path = Visitors; + sourceTree = ""; + }; 40BBC47A2C6227DF002AEF92 /* Extensions */ = { isa = PBXGroup; children = ( @@ -7158,6 +7183,7 @@ 4067F3192CDA469F002E28BD /* MockAudioSession.swift in Sources */, 40AB34C92C5D3F2E00B5B6B3 /* ParticipantsStats+Dummy.swift in Sources */, 84F58B7629EE92BF00010C4C /* UniqueValues.swift in Sources */, + 40B48C512D14F7AE002C4EAB /* SDPParser_Tests.swift in Sources */, 84F58B9529EEBA3900010C4C /* EquatableEvent.swift in Sources */, 40A0E9682B88E04D0089E8D3 /* CIImage_Resize_Tests.swift in Sources */, 40C9E4572C98B06E00802B28 /* WebRTCConfiguration_Tests.swift in Sources */, @@ -7210,6 +7236,7 @@ 406B3C532C92007900FC93A1 /* WebRTCCoordinatorStateMachine_ConnectedStageTests.swift in Sources */, 40B48C172D14C97F002C4EAB /* CGSize_DefaultValuesTests.swift in Sources */, 40B48C342D14D3E6002C4EAB /* StreamVideoSfuSignalTrackSubscriptionDetails_ConvenienceTests.swift in Sources */, + 40B48C4F2D14F77B002C4EAB /* SupportedPrefix_Tests.swift in Sources */, 84F58B8129EE9C4900010C4C /* WebSocketPingController_Delegate.swift in Sources */, 40AB34D02C5D443F00B5B6B3 /* MockWebSocketEngine.swift in Sources */, 40B48C322D14D361002C4EAB /* StreamVideoSfuModelsVideoLayer_ConvenienceTests.swift in Sources */, @@ -7270,6 +7297,7 @@ 84DC44982BA3ACC70050290C /* CallStatsReporting_Tests.swift in Sources */, 84F58B7229EE922700010C4C /* WebSocketConnectionState_Tests.swift in Sources */, 40FE5EBD2C9C82A6006B0881 /* MockRTCVideoCapturerDelegate.swift in Sources */, + 40B48C4C2D14F721002C4EAB /* RTPMapVisitorTests.swift in Sources */, 40B48C152D14C93B002C4EAB /* CGSize_AdaptTests.swift in Sources */, 40382F3D2C89C11D00C2D00F /* MockRTCPeerConnectionCoordinatorFactory.swift in Sources */, 40AB31262A49838000C270E1 /* EventTests.swift in Sources */, diff --git a/StreamVideoTests/WebRTC/SFU/SFUEventAdapter_Tests.swift b/StreamVideoTests/WebRTC/SFU/SFUEventAdapter_Tests.swift index 0c13ab2bd..b22c93b9a 100644 --- a/StreamVideoTests/WebRTC/SFU/SFUEventAdapter_Tests.swift +++ b/StreamVideoTests/WebRTC/SFU/SFUEventAdapter_Tests.swift @@ -562,6 +562,30 @@ final class SFUEventAdapter_Tests: XCTestCase, @unchecked Sendable { } } + // MARK: publishOptionsChanged + + func test_handleChangePublishOptions_givenEvent_whenPublished_thenUpdatesPublishOptions() async throws { + try await stateAdapter.configurePeerConnections() + let publisher = await stateAdapter.publisher + + let participantA = CallParticipant.dummy() + let participantB = CallParticipant.dummy() + var event = Stream_Video_Sfu_Event_ChangePublishOptions() + var option = Stream_Video_Sfu_Models_PublishOption() + option.bitrate = 100 + option.codec = .dummy(name: "av1") + option.trackType = .video + event.publishOptions = [option] + event.reason = .unique + let expected = PublishOptions(event.publishOptions) + + try await assert( + event, + wrappedEvent: .sfuEvent(.changePublishOptions(event)), + initialState: [participantA, participantB].reduce(into: [String: CallParticipant]()) { $0[$1.sessionId] = $1 } + ) { _ in await self.stateAdapter.publishOptions == expected } + } + // MARK: - Private helpers private func assert( diff --git a/StreamVideoTests/WebRTC/v2/SDP Parsing/SDPParser_Tests.swift b/StreamVideoTests/WebRTC/v2/SDP Parsing/SDPParser_Tests.swift new file mode 100644 index 000000000..0d955680c --- /dev/null +++ b/StreamVideoTests/WebRTC/v2/SDP Parsing/SDPParser_Tests.swift @@ -0,0 +1,57 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +@testable import StreamVideo +import XCTest + +final class SDPParser_Tests: XCTestCase { + + private var visitor: RTPMapVisitor! = .init() + private var subject: SDPParser! = .init() + + override func setUp() { + super.setUp() + subject.registerVisitor(visitor) + } + + override func tearDown() { + subject = nil + visitor = nil + super.tearDown() + } + + // MARK: - parse(sdp:) + + func test_parse_withValidSDP() async { + let sdp = "v=0\r\no=- 46117317 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=rtpmap:96 opus/48000/2\r\na=rtpmap:97 VP8/90000\r\n" + await subject.parse(sdp: sdp) + XCTAssertEqual(visitor.payloadType(for: "opus"), 96) + XCTAssertEqual(visitor.payloadType(for: "vp8"), 97) + } + + func test_parse_withInvalidSDP() async { + let sdp = """ + v=0 + o=- 46117317 2 IN IP4 127.0.0.1 + s=- + t=0 0 + a=invalid:96 opus/48000/2 + """ + await subject.parse(sdp: sdp) + XCTAssertNil(visitor.payloadType(for: "opus")) + } + + func test_parse_withMultipleVisitors() async { + let sdp = "v=0\r\no=- 46117317 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=rtpmap:96 opus/48000/2\r\na=rtpmap:97 VP8/90000\r\n" + let visitor1 = RTPMapVisitor() + let visitor2 = RTPMapVisitor() + subject.registerVisitor(visitor1) + subject.registerVisitor(visitor2) + await subject.parse(sdp: sdp) + XCTAssertEqual(visitor1.payloadType(for: "opus"), 96) + XCTAssertEqual(visitor1.payloadType(for: "vp8"), 97) + XCTAssertEqual(visitor2.payloadType(for: "opus"), 96) + XCTAssertEqual(visitor2.payloadType(for: "vp8"), 97) + } +} diff --git a/StreamVideoTests/WebRTC/v2/SDP Parsing/SupportedPrefix_Tests.swift b/StreamVideoTests/WebRTC/v2/SDP Parsing/SupportedPrefix_Tests.swift new file mode 100644 index 000000000..6a4570e2a --- /dev/null +++ b/StreamVideoTests/WebRTC/v2/SDP Parsing/SupportedPrefix_Tests.swift @@ -0,0 +1,22 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +@testable import StreamVideo +import XCTest + +final class SupportedPrefix_Tests: XCTestCase { + + // MARK: - SupportedPrefix Cases + + func test_supportedPrefixCases() { + XCTAssertEqual(SupportedPrefix.rtmap.rawValue, "a=rtpmap:") + } + + // MARK: - SupportedPrefix Set + + func test_supportedPrefixSetContainsRtmap() { + let supportedPrefixes: Set = [.rtmap] + XCTAssertTrue(supportedPrefixes.contains(.rtmap)) + } +} diff --git a/StreamVideoTests/WebRTC/v2/SDP Parsing/Visitors/RTPMapVisitorTests.swift b/StreamVideoTests/WebRTC/v2/SDP Parsing/Visitors/RTPMapVisitorTests.swift new file mode 100644 index 000000000..3690438c1 --- /dev/null +++ b/StreamVideoTests/WebRTC/v2/SDP Parsing/Visitors/RTPMapVisitorTests.swift @@ -0,0 +1,60 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +@testable import StreamVideo +import XCTest + +final class RTPMapVisitor_Tests: XCTestCase { + + private var subject: RTPMapVisitor! = .init() + + override func tearDown() { + subject = nil + super.tearDown() + } + + // MARK: - visit(line:) + + func test_visit_withValidRTPMapLine() { + let line = "a=rtpmap:96 opus/48000/2" + subject.visit(line: line) + XCTAssertEqual(subject.payloadType(for: "opus"), 96) + } + + func test_visit_withInvalidRTPMapLine() { + let line = "a=rtpmap:invalid opus/48000/2" + subject.visit(line: line) + XCTAssertNil(subject.payloadType(for: "opus")) + } + + func test_visit_withMissingCodecName() { + let line = "a=rtpmap:96" + subject.visit(line: line) + XCTAssertNil(subject.payloadType(for: "")) + } + + func test_visit_withMultipleRTPMapLines() { + let lines = [ + "a=rtpmap:96 opus/48000/2", + "a=rtpmap:97 VP8/90000", + "a=rtpmap:98 H264/90000" + ] + lines.forEach { subject.visit(line: $0) } + XCTAssertEqual(subject.payloadType(for: "opus"), 96) + XCTAssertEqual(subject.payloadType(for: "vp8"), 97) + XCTAssertEqual(subject.payloadType(for: "h264"), 98) + } + + // MARK: - payloadType(for:) + + func test_payloadType_forExistingCodec() { + let line = "a=rtpmap:96 opus/48000/2" + subject.visit(line: line) + XCTAssertEqual(subject.payloadType(for: "opus"), 96) + } + + func test_payloadType_forNonExistingCodec() { + XCTAssertNil(subject.payloadType(for: "nonexistent")) + } +} diff --git a/StreamVideoTests/WebRTC/v2/WebRTCAuthenticator_Tests.swift b/StreamVideoTests/WebRTC/v2/WebRTCAuthenticator_Tests.swift index 07ee3b2ed..ba6257cae 100644 --- a/StreamVideoTests/WebRTC/v2/WebRTCAuthenticator_Tests.swift +++ b/StreamVideoTests/WebRTC/v2/WebRTCAuthenticator_Tests.swift @@ -5,300 +5,298 @@ @testable import StreamVideo import XCTest -// final class WebRTCAuthenticator_Tests: XCTestCase { -// -// private static var videoConfig: VideoConfig! = .dummy() -// -// private lazy var mockCoordinatorStack: MockWebRTCCoordinatorStack! = .init( -// videoConfig: Self.videoConfig -// ) -// private lazy var subject: WebRTCAuthenticator! = .init() -// -// // MARK: - Lifecycle -// -// override class func tearDown() { -// Self.videoConfig = nil -// super.tearDown() -// } -// -// override func tearDown() { -// mockCoordinatorStack = nil -// subject = nil -// super.tearDown() -// } -// -// // MARK: - authenticate -// -// func test_authenticate_withValidData_callAuthenticationWasCalledWithExpectedInput() async throws { -// let currentSFU = String.unique -// let create = true -// let ring = true -// let notify = true -// let options = CreateCallOptions(team: .unique) -// let expected = JoinCallResponse.dummy() -// mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) -// -// _ = try await subject.authenticate( -// coordinator: mockCoordinatorStack.coordinator, -// currentSFU: currentSFU, -// create: create, -// ring: ring, -// notify: notify, -// options: options -// ) -// -// let input = try XCTUnwrap(mockCoordinatorStack.callAuthenticator.authenticateCalledWithInput.first) -// XCTAssertTrue(input.create) -// XCTAssertTrue(input.ring) -// XCTAssertTrue(input.notify) -// XCTAssertEqual(input.options?.team, options.team) -// } -// -// func test_authenticate_withValidData_shouldReturnSFUAdapterAndJoinCallResponse() async throws { -// let currentSFU = String.unique -// let create = true -// let ring = true -// let notify = true -// let options = CreateCallOptions() -// let expected = JoinCallResponse.dummy() -// mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) -// -// let (sfuAdapter, response) = try await subject.authenticate( -// coordinator: mockCoordinatorStack.coordinator, -// currentSFU: currentSFU, -// create: create, -// ring: ring, -// notify: notify, -// options: options -// ) -// -// XCTAssertEqual(response, expected) -// XCTAssertEqual(sfuAdapter.hostname, "getstream.io") -// XCTAssertEqual(sfuAdapter.connectURL.absoluteString, "wss://getstream.io") -// } -// -// func test_authenticate_withNilCurrentSFU_shouldStillReturnSFUAdapterAndJoinCallResponse() async throws { -// let create = true -// let ring = true -// let notify = true -// let options = CreateCallOptions() -// let expected = JoinCallResponse.dummy() -// mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) -// -// let (sfuAdapter, response) = try await subject.authenticate( -// coordinator: mockCoordinatorStack.coordinator, -// currentSFU: nil, -// create: create, -// ring: ring, -// notify: notify, -// options: options -// ) -// -// XCTAssertEqual(response, expected) -// XCTAssertEqual(sfuAdapter.hostname, "getstream.io") -// XCTAssertEqual(sfuAdapter.connectURL.absoluteString, "wss://getstream.io") -// } -// -// func test_authenticate_withCreateTrueAndInitialCallSettings_shouldSetInitialCallSettings() async throws { -// let create = true -// let ring = true -// let notify = true -// let options = CreateCallOptions() -// let expected = JoinCallResponse.dummy(call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false)))) -// mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) -// let initialCallSettings = CallSettings( -// audioOn: true, -// videoOn: true, -// speakerOn: true -// ) -// await mockCoordinatorStack -// .coordinator -// .stateAdapter -// .set(initialCallSettings: initialCallSettings) -// -// _ = try await subject.authenticate( -// coordinator: mockCoordinatorStack.coordinator, -// currentSFU: nil, -// create: create, -// ring: ring, -// notify: notify, -// options: options -// ) -// -// let callSettings = await mockCoordinatorStack -// .coordinator -// .stateAdapter -// .callSettings -// XCTAssertEqual(callSettings, initialCallSettings) -// } -// -// func test_authenticate_withCreateTrueWithoutInitialCallSettings_shouldSetCallSettingsFromResponse() async throws { -// let create = true -// let ring = true -// let notify = true -// let options = CreateCallOptions() -// let expected = JoinCallResponse.dummy(call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: true)))) -// mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) -// -// _ = try await subject.authenticate( -// coordinator: mockCoordinatorStack.coordinator, -// currentSFU: nil, -// create: create, -// ring: ring, -// notify: notify, -// options: options -// ) -// -// let callSettings = await mockCoordinatorStack -// .coordinator -// .stateAdapter -// .callSettings -// XCTAssertEqual(callSettings, expected.call.settings.toCallSettings) -// } -// -// func test_authenticate_withCreateFalse_shouldNotSetInitialCallSettings() async throws { -// let create = false -// let ring = true -// let notify = true -// let options = CreateCallOptions() -// let expected = JoinCallResponse.dummy(call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false)))) -// mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) -// await mockCoordinatorStack -// .coordinator -// .stateAdapter -// .set(initialCallSettings: .init(audioOn: false, audioOutputOn: false)) -// -// _ = try await subject.authenticate( -// coordinator: mockCoordinatorStack.coordinator, -// currentSFU: nil, -// create: create, -// ring: ring, -// notify: notify, -// options: options -// ) -// -// let callSettings = await mockCoordinatorStack -// .coordinator -// .stateAdapter -// .callSettings -// XCTAssertTrue(callSettings.audioOn) -// XCTAssertTrue(callSettings.audioOutputOn) -// } -// -// func test_authenticate_updatesVideoOptions() async throws { -// let create = false -// let ring = true -// let notify = true -// let options = CreateCallOptions() -// let expected = JoinCallResponse.dummy( -// call: .dummy( -// settings: .dummy( -// video: .dummy( -// cameraFacing: .back, -// targetResolution: .init(bitrate: 100, height: 200, width: 300) -// ) -// ) -// ) -// ) -// mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) -// -// _ = try await subject.authenticate( -// coordinator: mockCoordinatorStack.coordinator, -// currentSFU: nil, -// create: create, -// ring: ring, -// notify: notify, -// options: options -// ) -// -// let videoOptions = await mockCoordinatorStack -// .coordinator -// .stateAdapter -// .videoOptions -// XCTAssertEqual(videoOptions.preferredCameraPosition, .back) -// XCTAssertEqual(videoOptions.preferredDimensions.height, 200) -// XCTAssertEqual(videoOptions.preferredDimensions.width, 300) -// } -// -// func test_authenticate_updatesIntervalOnStatsReporter() async throws { -// let create = false -// let ring = true -// let notify = true -// let options = CreateCallOptions() -// let expected = JoinCallResponse.dummy( -// statsOptions: .init(reportingIntervalMs: 12000) -// ) -// mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) -// -// _ = try await subject.authenticate( -// coordinator: mockCoordinatorStack.coordinator, -// currentSFU: nil, -// create: create, -// ring: ring, -// notify: notify, -// options: options -// ) -// -// let statsReporter = await mockCoordinatorStack -// .coordinator -// .stateAdapter -// .statsReporter -// XCTAssertEqual(statsReporter?.interval, 12) -// } -// -// // MARK: - waitForAuthentication -// -// func test_waitForAuthentication_shouldThrowErrorIfTimeout() async throws { -// _ = await XCTAssertThrowsErrorAsync { -// try await subject -// .waitForAuthentication(on: mockCoordinatorStack.sfuStack.adapter) -// } -// } -// -// func test_waitForAuthentication_shouldWaitUntilConnectionStateIsAuthenticating() async throws { -// try await withThrowingTaskGroup(of: Void.self) { group in -// -// group.addTask { -// try await self.subject -// .waitForAuthentication(on: self.mockCoordinatorStack.sfuStack.adapter) -// } -// -// group.addTask { -// await self.wait(for: 0.5) -// self.mockCoordinatorStack -// .sfuStack -// .setConnectionState(to: .authenticating) -// } -// -// try await group.waitForAll() -// } -// } -// -// // MARK: - waitForConnect -// -// func test_waitForConnect_shouldThrowErrorIfTimeout() async throws { -// _ = await XCTAssertThrowsErrorAsync { -// try await subject -// .waitForConnect(on: mockCoordinatorStack.sfuStack.adapter) -// } -// } -// -// func test_waitForConnect_shouldWaitUntilConnectionStateIsConnected() async throws { -// try await withThrowingTaskGroup(of: Void.self) { group in -// -// group.addTask { -// try await self.subject -// .waitForConnect(on: self.mockCoordinatorStack.sfuStack.adapter) -// } -// -// group.addTask { -// await self.wait(for: 0.5) -// self.mockCoordinatorStack -// .sfuStack -// .setConnectionState(to: .connected(healthCheckInfo: .init())) -// } -// -// try await group.waitForAll() -// } -// } -// } +final class WebRTCAuthenticator_Tests: XCTestCase { + + private static var videoConfig: VideoConfig! = .dummy() + + private lazy var mockCoordinatorStack: MockWebRTCCoordinatorStack! = .init( + videoConfig: Self.videoConfig + ) + private lazy var subject: WebRTCAuthenticator! = .init() + + // MARK: - Lifecycle + + override class func tearDown() { + Self.videoConfig = nil + super.tearDown() + } + + override func tearDown() { + mockCoordinatorStack = nil + subject = nil + super.tearDown() + } + + // MARK: - authenticate + + func test_authenticate_withValidData_callAuthenticationWasCalledWithExpectedInput() async throws { + let currentSFU = String.unique + let create = true + let ring = true + let notify = true + let options = CreateCallOptions(team: .unique) + let expected = JoinCallResponse.dummy() + mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) + + _ = try await subject.authenticate( + coordinator: mockCoordinatorStack.coordinator, + currentSFU: currentSFU, + create: create, + ring: ring, + notify: notify, + options: options + ) + + let input = try XCTUnwrap(mockCoordinatorStack.callAuthenticator.authenticateCalledWithInput.first) + XCTAssertTrue(input.create) + XCTAssertTrue(input.ring) + XCTAssertTrue(input.notify) + XCTAssertEqual(input.options?.team, options.team) + } + + func test_authenticate_withValidData_shouldReturnSFUAdapterAndJoinCallResponse() async throws { + let currentSFU = String.unique + let create = true + let ring = true + let notify = true + let options = CreateCallOptions() + let expected = JoinCallResponse.dummy() + mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) + + let (sfuAdapter, response) = try await subject.authenticate( + coordinator: mockCoordinatorStack.coordinator, + currentSFU: currentSFU, + create: create, + ring: ring, + notify: notify, + options: options + ) + + XCTAssertEqual(response, expected) + XCTAssertEqual(sfuAdapter.hostname, "getstream.io") + XCTAssertEqual(sfuAdapter.connectURL.absoluteString, "wss://getstream.io") + } + + func test_authenticate_withNilCurrentSFU_shouldStillReturnSFUAdapterAndJoinCallResponse() async throws { + let create = true + let ring = true + let notify = true + let options = CreateCallOptions() + let expected = JoinCallResponse.dummy() + mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) + + let (sfuAdapter, response) = try await subject.authenticate( + coordinator: mockCoordinatorStack.coordinator, + currentSFU: nil, + create: create, + ring: ring, + notify: notify, + options: options + ) + + XCTAssertEqual(response, expected) + XCTAssertEqual(sfuAdapter.hostname, "getstream.io") + XCTAssertEqual(sfuAdapter.connectURL.absoluteString, "wss://getstream.io") + } + + func test_authenticate_withCreateTrueAndInitialCallSettings_shouldSetInitialCallSettings() async throws { + let create = true + let ring = true + let notify = true + let options = CreateCallOptions() + let expected = JoinCallResponse.dummy(call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false)))) + mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) + let initialCallSettings = CallSettings( + audioOn: true, + videoOn: true, + speakerOn: true + ) + await mockCoordinatorStack + .coordinator + .stateAdapter + .set(initialCallSettings: initialCallSettings) + + _ = try await subject.authenticate( + coordinator: mockCoordinatorStack.coordinator, + currentSFU: nil, + create: create, + ring: ring, + notify: notify, + options: options + ) + + let callSettings = await mockCoordinatorStack + .coordinator + .stateAdapter + .callSettings + XCTAssertEqual(callSettings, initialCallSettings) + } + + func test_authenticate_withCreateTrueWithoutInitialCallSettings_shouldSetCallSettingsFromResponse() async throws { + let create = true + let ring = true + let notify = true + let options = CreateCallOptions() + let expected = JoinCallResponse.dummy(call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: true)))) + mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) + + _ = try await subject.authenticate( + coordinator: mockCoordinatorStack.coordinator, + currentSFU: nil, + create: create, + ring: ring, + notify: notify, + options: options + ) + + let callSettings = await mockCoordinatorStack + .coordinator + .stateAdapter + .callSettings + XCTAssertEqual(callSettings, expected.call.settings.toCallSettings) + } + + func test_authenticate_withCreateFalse_shouldNotSetInitialCallSettings() async throws { + let create = false + let ring = true + let notify = true + let options = CreateCallOptions() + let expected = JoinCallResponse.dummy(call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false)))) + mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) + await mockCoordinatorStack + .coordinator + .stateAdapter + .set(initialCallSettings: .init(audioOn: false, audioOutputOn: false)) + + _ = try await subject.authenticate( + coordinator: mockCoordinatorStack.coordinator, + currentSFU: nil, + create: create, + ring: ring, + notify: notify, + options: options + ) + + let callSettings = await mockCoordinatorStack + .coordinator + .stateAdapter + .callSettings + XCTAssertTrue(callSettings.audioOn) + XCTAssertTrue(callSettings.audioOutputOn) + } + + func test_authenticate_updatesVideoOptions() async throws { + let create = false + let ring = true + let notify = true + let options = CreateCallOptions() + let expected = JoinCallResponse.dummy( + call: .dummy( + settings: .dummy( + video: .dummy( + cameraFacing: .back, + targetResolution: .init(bitrate: 100, height: 200, width: 300) + ) + ) + ) + ) + mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) + + _ = try await subject.authenticate( + coordinator: mockCoordinatorStack.coordinator, + currentSFU: nil, + create: create, + ring: ring, + notify: notify, + options: options + ) + + let videoOptions = await mockCoordinatorStack + .coordinator + .stateAdapter + .videoOptions + XCTAssertEqual(videoOptions.preferredCameraPosition, .back) + } + + func test_authenticate_updatesIntervalOnStatsReporter() async throws { + let create = false + let ring = true + let notify = true + let options = CreateCallOptions() + let expected = JoinCallResponse.dummy( + statsOptions: .init(reportingIntervalMs: 12000) + ) + mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) + + _ = try await subject.authenticate( + coordinator: mockCoordinatorStack.coordinator, + currentSFU: nil, + create: create, + ring: ring, + notify: notify, + options: options + ) + + let statsReporter = await mockCoordinatorStack + .coordinator + .stateAdapter + .statsReporter + XCTAssertEqual(statsReporter?.interval, 12) + } + + // MARK: - waitForAuthentication + + func test_waitForAuthentication_shouldThrowErrorIfTimeout() async throws { + _ = await XCTAssertThrowsErrorAsync { + try await subject + .waitForAuthentication(on: mockCoordinatorStack.sfuStack.adapter) + } + } + + func test_waitForAuthentication_shouldWaitUntilConnectionStateIsAuthenticating() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + + group.addTask { + try await self.subject + .waitForAuthentication(on: self.mockCoordinatorStack.sfuStack.adapter) + } + + group.addTask { + await self.wait(for: 0.5) + self.mockCoordinatorStack + .sfuStack + .setConnectionState(to: .authenticating) + } + + try await group.waitForAll() + } + } + + // MARK: - waitForConnect + + func test_waitForConnect_shouldThrowErrorIfTimeout() async throws { + _ = await XCTAssertThrowsErrorAsync { + try await subject + .waitForConnect(on: mockCoordinatorStack.sfuStack.adapter) + } + } + + func test_waitForConnect_shouldWaitUntilConnectionStateIsConnected() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + + group.addTask { + try await self.subject + .waitForConnect(on: self.mockCoordinatorStack.sfuStack.adapter) + } + + group.addTask { + await self.wait(for: 0.5) + self.mockCoordinatorStack + .sfuStack + .setConnectionState(to: .connected(healthCheckInfo: .init())) + } + + try await group.waitForAll() + } + } +} diff --git a/StreamVideoTests/WebRTC/v2/WebRTCCoorindator_Tests.swift b/StreamVideoTests/WebRTC/v2/WebRTCCoorindator_Tests.swift index e6e016b1e..3dcf7ac55 100644 --- a/StreamVideoTests/WebRTC/v2/WebRTCCoorindator_Tests.swift +++ b/StreamVideoTests/WebRTC/v2/WebRTCCoorindator_Tests.swift @@ -7,640 +7,642 @@ import Combine import StreamWebRTC @preconcurrency import XCTest -// final class WebRTCCoordinator_Tests: XCTestCase, @unchecked Sendable { -// /// Class variable that will be used by all test cases in the file. This ensure that only one -// /// PeerConnectionFactory will be created during tests, ensuring that WebRTC deallocation will -// /// only happen once all tests cases in the file ran. -// private static var videoConfig: VideoConfig! = .dummy() -// -// private lazy var user: User! = .dummy() -// private lazy var apiKey: String! = .unique -// private lazy var callCid: String! = .unique -// private lazy var mockCallAuthenticator: MockCallAuthenticator! = .init() -// private lazy var mockWebRTCAuthenticator: MockWebRTCAuthenticator! = .init() -// private lazy var rtcPeerConnectionCoordinatorFactory: MockRTCPeerConnectionCoordinatorFactory! = .init() -// private lazy var mockSFUStack: MockSFUStack! = .init() -// private lazy var subject: WebRTCCoordinator! = .init( -// user: user, -// apiKey: apiKey, -// callCid: callCid, -// videoConfig: Self.videoConfig, -// rtcPeerConnectionCoordinatorFactory: rtcPeerConnectionCoordinatorFactory, -// webRTCAuthenticator: mockWebRTCAuthenticator, -// callAuthentication: mockCallAuthenticator.authenticate -// ) -// -// // MARK: - Lifecycle -// -// override class func tearDown() { -// Self.videoConfig = nil -// super.tearDown() -// } -// -// override func tearDown() async throws { -// subject = nil -// mockSFUStack = nil -// rtcPeerConnectionCoordinatorFactory = nil -// mockCallAuthenticator = nil -// callCid = nil -// apiKey = nil -// user = nil -// try await super.tearDown() -// } -// -// // MARK: - connect -// -// func test_connect_shouldSetInitialCallSettingsAndTransitionStateMachine() async throws { -// let expectedCallSettings = CallSettings(cameraPosition: .back) -// let expectedOptions = CreateCallOptions( -// memberIds: [.unique, .unique], -// members: [.init(userId: .unique)], -// custom: [.unique: .bool(true)], -// settings: CallSettingsRequest(audio: .init(defaultDevice: .earpiece)), -// startsAt: .init(timeIntervalSince1970: 100), -// team: .unique -// ) -// -// try await assertTransitionToStage( -// .connecting, -// operation: { -// try await self -// .subject -// .connect( -// callSettings: expectedCallSettings, -// options: expectedOptions, -// ring: true, -// notify: true -// ) -// } -// ) { stage in -// let expectedStage = try XCTUnwrap(stage as? WebRTCCoordinator.StateMachine.Stage.ConnectingStage) -// XCTAssertEqual(expectedStage.options?.memberIds, expectedOptions.memberIds) -// XCTAssertEqual(expectedStage.options?.members, expectedOptions.members) -// XCTAssertEqual(expectedStage.options?.custom, expectedOptions.custom) -// XCTAssertEqual(expectedStage.options?.settings?.audio?.defaultDevice, .earpiece) -// XCTAssertEqual(expectedStage.options?.startsAt, expectedOptions.startsAt) -// XCTAssertEqual(expectedStage.options?.team, expectedOptions.team) -// XCTAssertTrue(expectedStage.ring) -// XCTAssertTrue(expectedStage.notify) -// await self.assertEqualAsync( -// await self.subject.stateAdapter.initialCallSettings, -// expectedCallSettings -// ) -// } -// } -// -// // MARK: - cleanUp -// -// func test_cleanUp_shouldCallStateAdapterCleanUp() async throws { -// try await prepareAsConnected() -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// let mockSubscriber = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .subscriber as? MockRTCPeerConnectionCoordinator -// ) -// -// await subject.cleanUp() -// -// XCTAssertEqual(mockPublisher.timesCalled(.close), 1) -// XCTAssertEqual(mockSubscriber.timesCalled(.close), 1) -// XCTAssertEqual(mockSFUStack.webSocket.timesCalled(.disconnectAsync), 1) -// await assertNilAsync(await subject.stateAdapter.publisher) -// await assertNilAsync(await subject.stateAdapter.subscriber) -// await assertNilAsync(await subject.stateAdapter.statsReporter) -// await assertNilAsync(await subject.stateAdapter.sfuAdapter) -// await assertEqualAsync(await subject.stateAdapter.token, "") -// await assertEqualAsync(await subject.stateAdapter.sessionID, "") -// await assertEqualAsync(await subject.stateAdapter.ownCapabilities, []) -// await assertEqualAsync(await subject.stateAdapter.participants, [:]) -// await assertEqualAsync(await subject.stateAdapter.participantsCount, 0) -// await assertEqualAsync(await subject.stateAdapter.anonymousCount, 0) -// await assertEqualAsync(await subject.stateAdapter.participantPins, []) -// } -// -// // MARK: - leave -// -// func test_leave_shouldTransitionStateMachineToLeaving() async throws { -// mockWebRTCAuthenticator -// .stub( -// for: .authenticate, -// with: Result<(SFUAdapter, JoinCallResponse), Error> -// .success((mockSFUStack.adapter, JoinCallResponse.dummy())) -// ) -// mockWebRTCAuthenticator.stub( -// for: .waitForAuthentication, -// with: Result.success(()) -// ) -// -// try await assertTransitionToStage( -// .connected, -// operation: { -// try await self -// .subject -// .connect( -// callSettings: nil, -// options: nil, -// ring: true, -// notify: true -// ) -// } -// ) { _ in -// await self.assertTransitionToStage(.leaving) { -// self.subject.leave() -// } handler: { _ in } -// } -// } -// -// // MARK: - changeCameraMode -// -// func test_changeCameraMode_shouldUpdateCameraPosition() async throws { -// try await prepareAsConnected() -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// -// try await subject.changeCameraMode(position: .back) -// -// await assertEqualAsync( -// await subject.stateAdapter.callSettings.cameraPosition, -// .back -// ) -// XCTAssertEqual( -// mockPublisher.recordedInputPayload( -// AVCaptureDevice.Position.self, -// for: .didUpdateCameraPosition -// )?.first, -// .back -// ) -// } -// -// // MARK: - changeAudioState -// -// func test_changeAudioState_shouldUpdateAudioState() async throws { -// await subject.changeAudioState(isEnabled: false) -// -// await assertEqualAsync( -// await subject.stateAdapter.callSettings.audioOn, -// false -// ) -// } -// -// // MARK: - changeSoundState -// -// func test_changeSoundState_shouldUpdateAudioOutputState() async throws { -// await subject.changeSoundState(isEnabled: false) -// -// await assertEqualAsync( -// await subject.stateAdapter.callSettings.audioOutputOn, -// false -// ) -// } -// -// // MARK: - changeSpeakerState -// -// func test_changeSpeakerState_shouldUpdateSpeakerState() async throws { -// await subject.changeSpeakerState(isEnabled: false) -// -// await assertEqualAsync( -// await subject.stateAdapter.callSettings.speakerOn, -// false -// ) -// } -// -// // MARK: - changeTrackVisibility -// -// func test_changeTrackVisibility_shouldUpdateParticipantTrackVisibility() async throws { -// try await prepareAsConnected() -// -// await subject.changeTrackVisibility( -// for: .dummy(id: user.id), -// isVisible: true -// ) -// -// await fulfillment { -// await self -// .subject -// .stateAdapter -// .participants[self.user.id]? -// .showTrack == true -// } -// } -// -// // MARK: - updateTrackSize -// -// func test_updateTrackSize_shouldUpdateParticipantTrackSize() async throws { -// try await prepareAsConnected() -// -// await subject.updateTrackSize( -// .init(width: 100, height: 200), -// for: .dummy(id: user.id) -// ) -// -// await fulfillment { -// await self -// .subject -// .stateAdapter -// .participants[self.user.id]? -// .trackSize == .init(width: 100, height: 200) -// } -// } -// -// // MARK: - setVideoFilter -// -// func test_setVideoFilter_shouldSetVideoFilter() async throws { -// let expected = VideoFilter(id: .unique, name: .unique, filter: { _ in fatalError() }) -// try await prepareAsConnected(videoFilter: nil) -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// -// await subject.setVideoFilter(expected) -// -// let actual = try XCTUnwrap(mockPublisher.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.last) -// XCTAssertEqual(actual.id, expected.id) -// XCTAssertEqual(actual.name, expected.name) -// } -// -// // MARK: - startScreensharing -// -// func test_startScreensharing_typeIsInApp_shouldBeginScreenSharing() async throws { -// try await prepareAsConnected() -// let ownCapabilities = [OwnCapability.createReaction] -// await subject.stateAdapter.set(ownCapabilities: Set(ownCapabilities)) -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// -// try await subject.startScreensharing(type: .inApp) -// -// let actual = try XCTUnwrap( -// mockPublisher.recordedInputPayload( -// (ScreensharingType, [OwnCapability]).self, -// for: .beginScreenSharing -// )?.first -// ) -// XCTAssertEqual(actual.0, .inApp) -// XCTAssertEqual(actual.1, ownCapabilities) -// } -// -// func test_startScreensharing_typeIsBroadcast_shouldBeginScreenSharing() async throws { -// try await prepareAsConnected() -// let ownCapabilities = [OwnCapability.createReaction] -// await subject.stateAdapter.set(ownCapabilities: Set(ownCapabilities)) -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// -// try await subject.startScreensharing(type: .broadcast) -// -// let actual = try XCTUnwrap( -// mockPublisher.recordedInputPayload( -// (ScreensharingType, [OwnCapability]).self, -// for: .beginScreenSharing -// )?.first -// ) -// XCTAssertEqual(actual.0, .broadcast) -// XCTAssertEqual(actual.1, ownCapabilities) -// } -// -// // MARK: - stopScreensharing -// -// func test_stopScreensharing_shouldStopScreenSharing() async throws { -// try await prepareAsConnected() -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// -// try await subject.stopScreensharing() -// -// XCTAssertEqual(mockPublisher.timesCalled(.stopScreenSharing), 1) -// } -// -// // MARK: - changePinState -// -// func test_changePinState_isEnabledTrue_shouldUpdateParticipantPin() async throws { -// try await prepareAsConnected() -// -// try await subject.changePinState(isEnabled: true, sessionId: user.id) -// -// await fulfillment { -// await self -// .subject -// .stateAdapter -// .participants[self.user.id]? -// .pin?.isLocal == true -// } -// } -// -// func test_changePinState_isEnabledFalse_shouldUpdateParticipantPin() async throws { -// try await prepareAsConnected() -// try await subject.changePinState(isEnabled: true, sessionId: user.id) -// -// try await subject.changePinState(isEnabled: false, sessionId: user.id) -// -// await fulfillment { -// await self -// .subject -// .stateAdapter -// .participants[self.user.id]? -// .pin == nil -// } -// } -// -// // MARK: - startNoiseCancellation -// -// func test_startNoiseCancellation_shouldEnableNoiseCancellationForSession() async throws { -// try await prepareAsConnected() -// -// try await subject.startNoiseCancellation(user.id) -// -// XCTAssertEqual( -// mockSFUStack.service.startNoiseCancellationWasCalledWithRequest?.sessionID, -// user.id -// ) -// } -// -// // MARK: - stopNoiseCancellation -// -// func test_stopNoiseCancellation_shouldDisableNoiseCancellationForSession() async throws { -// try await prepareAsConnected() -// -// try await subject.stopNoiseCancellation(user.id) -// -// XCTAssertEqual( -// mockSFUStack.service.stopNoiseCancellationWasCalledWithRequest?.sessionID, -// user.id -// ) -// } -// -// // MARK: - focus -// -// func test_focus_shouldFocusOnSpecifiedPoint() async throws { -// try await prepareAsConnected() -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// -// try await subject.focus(at: .init(x: 10, y: 20)) -// -// XCTAssertEqual( -// mockPublisher.recordedInputPayload(CGPoint.self, for: .focus)?.first, -// .init(x: 10, y: 20) -// ) -// } -// -// // MARK: - addCapturePhotoOutput -// -// func test_addCapturePhotoOutput_shouldAddPhotoOutputToCaptureSession() async throws { -// try await prepareAsConnected() -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// let expected = AVCapturePhotoOutput() -// -// try await subject.addCapturePhotoOutput(expected) -// -// XCTAssertTrue( -// mockPublisher.recordedInputPayload(AVCapturePhotoOutput.self, for: .addCapturePhotoOutput)?.first === expected -// ) -// } -// -// // MARK: - removeCapturePhotoOutput -// -// func test_removeCapturePhotoOutput_shouldRemovePhotoOutputFromCaptureSession() async throws { -// try await prepareAsConnected() -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// let expected = AVCapturePhotoOutput() -// -// try await subject.removeCapturePhotoOutput(expected) -// -// XCTAssertTrue( -// mockPublisher.recordedInputPayload(AVCapturePhotoOutput.self, for: .removeCapturePhotoOutput)?.first === expected -// ) -// } -// -// // MARK: - addVideoOutput -// -// func test_addVideoOutput_shouldAddVideoOutputToCaptureSession() async throws { -// try await prepareAsConnected() -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// let expected = AVCaptureVideoDataOutput() -// -// try await subject.addVideoOutput(expected) -// -// XCTAssertTrue( -// mockPublisher.recordedInputPayload(AVCaptureVideoDataOutput.self, for: .addVideoOutput)?.first === expected -// ) -// } -// -// // MARK: - removeVideoOutput -// -// func test_removeVideoOutput_shouldRemoveVideoOutputFromCaptureSession() async throws { -// try await prepareAsConnected() -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// let expected = AVCaptureVideoDataOutput() -// -// try await subject.removeVideoOutput(expected) -// -// XCTAssertTrue( -// mockPublisher.recordedInputPayload(AVCaptureVideoDataOutput.self, for: .removeVideoOutput)?.first === expected -// ) -// } -// -// // MARK: - zoom -// -// func test_zoom_shouldZoomCameraBySpecifiedFactor() async throws { -// try await prepareAsConnected() -// let mockPublisher = try await XCTAsyncUnwrap( -// await subject -// .stateAdapter -// .publisher as? MockRTCPeerConnectionCoordinator -// ) -// -// try await subject.zoom(by: 32) -// -// XCTAssertEqual( -// mockPublisher.recordedInputPayload(CGFloat.self, for: .zoom)?.first, -// 32 -// ) -// } -// -// // MARK: - setIncomingVideoQualitySettings -// -// func test_setIncomingVideoQualitySettings_correctlyUpdatesStateAdapter() async throws { -// try await prepareAsConnected() -// let incomingVideoQualitySettings = IncomingVideoQualitySettings.manual( -// group: .custom(sessionIds: [.unique, .unique]), -// targetSize: .init( -// width: 11, -// height: 10 -// ) -// ) -// -// await subject.setIncomingVideoQualitySettings(incomingVideoQualitySettings) -// -// await assertEqualAsync( -// await subject.stateAdapter.incomingVideoQualitySettings, -// incomingVideoQualitySettings -// ) -// } -// -// // MARK: - setDisconnectionTimeout -// -// func test_setDisconnectionTimeout_correctlyUpdatesStageContext() async throws { -// try await prepareAsConnected() -// -// subject.setDisconnectionTimeout(11) -// -// XCTAssertEqual( -// subject.stateMachine.currentStage.context.disconnectionTimeout, -// 11 -// ) -// } -// -// // MARK: - updatePublishOptions -// -// func test_updatePublishOptions_shouldCallUpdatePublishOptionsCoordinator() async throws { -// try await prepareAsConnected() -// -// await subject.updatePublishOptions( -// preferredVideoCodec: .vp9, -// maxBitrate: 1000 -// ) -// -// let videoOptions = await subject -// .stateAdapter -// .videoOptions -// XCTAssertEqual(videoOptions.preferredVideoCodec, .vp9) -// XCTAssertEqual(videoOptions.preferredBitrate, 1000) -// } -// -// // MARK: - Private helpers -// -// private func assertEqualAsync( -// _ expression: @autoclosure () async throws -> T, -// _ expected: @autoclosure () async throws -> T, -// file: StaticString = #file, -// line: UInt = #line -// ) async rethrows { -// let value = try await expression() -// let expectedValue = try await expected() -// XCTAssertEqual(value, expectedValue, file: file, line: line) -// } -// -// private func assertNoThrowAsync( -// _ expression: @autoclosure () async throws -> Void, -// file: StaticString = #file, -// line: UInt = #line -// ) async { -// do { -// try await expression() -// } catch { -// let thrower = { throw error } -// XCTAssertNoThrow(try thrower(), file: file, line: line) -// } -// } -// -// private func assertTransitionToStage( -// _ id: WebRTCCoordinator.StateMachine.Stage.ID, -// operation: @escaping () async throws -> Void, -// handler: @escaping (WebRTCCoordinator.StateMachine.Stage) async throws -> Void, -// file: StaticString = #file, -// line: UInt = #line -// ) async rethrows { -// let transitionExpectation = expectation(description: "WebRTCCoordinator is expected to transition to stage id:\(id).") -// -// try await withThrowingTaskGroup(of: Void.self) { group in -// group.addTask { -// let target = try await self -// .subject -// .stateMachine -// .publisher -// .filter { $0.id == id } -// .nextValue(timeout: defaultTimeout) -// -// await self.assertNoThrowAsync( -// try await handler(target), -// file: file, -// line: line -// ) -// transitionExpectation.fulfill() -// } -// group.addTask { -// await self.wait(for: 0.1) -// try await operation() -// } -// group.addTask { -// await self.fulfillment(of: [transitionExpectation], timeout: defaultTimeout) -// } -// -// try await group.waitForAll() -// } -// } -// -// private func assertNilAsync( -// _ expression: @autoclosure () async throws -> T?, -// file: StaticString = #file, -// line: UInt = #line -// ) async rethrows { -// let value = try await expression() -// XCTAssertNil(value, file: file, line: line) -// } -// -// private func prepareAsConnected( -// videoFilter: VideoFilter? = VideoFilter( -// id: .unique, -// name: .unique, -// filter: { _ in fatalError() } -// ) -// ) async throws { -// mockSFUStack.setConnectionState(to: .connected(healthCheckInfo: .init())) -// let ownCapabilities = Set([OwnCapability.blockUsers, .changeMaxDuration]) -// let callSettings = CallSettings(cameraPosition: .back) -// await subject.stateAdapter.set(sfuAdapter: mockSFUStack.adapter) -// if let videoFilter { -// await subject.stateAdapter.set(videoFilter: videoFilter) -// } -// await subject.stateAdapter.set(ownCapabilities: ownCapabilities) -// await subject.stateAdapter.set(callSettings: callSettings) -// await subject.stateAdapter.set(sessionID: .unique) -// await subject.stateAdapter.set(token: .unique) -// await subject.stateAdapter.set(participantsCount: 12) -// await subject.stateAdapter.set(anonymousCount: 22) -// await subject.stateAdapter.set(participantPins: [PinInfo(isLocal: true, pinnedAt: .init())]) -// await subject.stateAdapter.enqueue { _ in [self.user.id: CallParticipant.dummy(id: self.user.id)] } -// try await subject.stateAdapter.configurePeerConnections() -// await subject.stateAdapter.set(statsReporter: WebRTCStatsReporter(sessionID: .unique)) -// } -// } +final class WebRTCCoordinator_Tests: XCTestCase, @unchecked Sendable { + /// Class variable that will be used by all test cases in the file. This ensure that only one + /// PeerConnectionFactory will be created during tests, ensuring that WebRTC deallocation will + /// only happen once all tests cases in the file ran. + private static var videoConfig: VideoConfig! = .dummy() + + private lazy var user: User! = .dummy() + private lazy var apiKey: String! = .unique + private lazy var callCid: String! = .unique + private lazy var mockCallAuthenticator: MockCallAuthenticator! = .init() + private lazy var mockWebRTCAuthenticator: MockWebRTCAuthenticator! = .init() + private lazy var rtcPeerConnectionCoordinatorFactory: MockRTCPeerConnectionCoordinatorFactory! = .init() + private lazy var mockSFUStack: MockSFUStack! = .init() + private lazy var subject: WebRTCCoordinator! = .init( + user: user, + apiKey: apiKey, + callCid: callCid, + videoConfig: Self.videoConfig, + rtcPeerConnectionCoordinatorFactory: rtcPeerConnectionCoordinatorFactory, + webRTCAuthenticator: mockWebRTCAuthenticator, + callAuthentication: mockCallAuthenticator.authenticate + ) + + // MARK: - Lifecycle + + override class func tearDown() { + Self.videoConfig = nil + super.tearDown() + } + + override func tearDown() async throws { + subject = nil + mockSFUStack = nil + rtcPeerConnectionCoordinatorFactory = nil + mockCallAuthenticator = nil + callCid = nil + apiKey = nil + user = nil + try await super.tearDown() + } + + // MARK: - connect + + func test_connect_shouldSetInitialCallSettingsAndTransitionStateMachine() async throws { + let expectedCallSettings = CallSettings(cameraPosition: .back) + let expectedOptions = CreateCallOptions( + memberIds: [.unique, .unique], + members: [.init(userId: .unique)], + custom: [.unique: .bool(true)], + settings: CallSettingsRequest(audio: .init(defaultDevice: .earpiece)), + startsAt: .init(timeIntervalSince1970: 100), + team: .unique + ) + + try await assertTransitionToStage( + .connecting, + operation: { + try await self + .subject + .connect( + callSettings: expectedCallSettings, + options: expectedOptions, + ring: true, + notify: true + ) + } + ) { stage in + let expectedStage = try XCTUnwrap(stage as? WebRTCCoordinator.StateMachine.Stage.ConnectingStage) + XCTAssertEqual(expectedStage.options?.memberIds, expectedOptions.memberIds) + XCTAssertEqual(expectedStage.options?.members, expectedOptions.members) + XCTAssertEqual(expectedStage.options?.custom, expectedOptions.custom) + XCTAssertEqual(expectedStage.options?.settings?.audio?.defaultDevice, .earpiece) + XCTAssertEqual(expectedStage.options?.startsAt, expectedOptions.startsAt) + XCTAssertEqual(expectedStage.options?.team, expectedOptions.team) + XCTAssertTrue(expectedStage.ring) + XCTAssertTrue(expectedStage.notify) + await self.assertEqualAsync( + await self.subject.stateAdapter.initialCallSettings, + expectedCallSettings + ) + } + } + + // MARK: - cleanUp + + func test_cleanUp_shouldCallStateAdapterCleanUp() async throws { + try await prepareAsConnected() + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + let mockSubscriber = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .subscriber as? MockRTCPeerConnectionCoordinator + ) + + await subject.cleanUp() + + XCTAssertEqual(mockPublisher.timesCalled(.close), 1) + XCTAssertEqual(mockSubscriber.timesCalled(.close), 1) + XCTAssertEqual(mockSFUStack.webSocket.timesCalled(.disconnectAsync), 1) + await assertNilAsync(await subject.stateAdapter.publisher) + await assertNilAsync(await subject.stateAdapter.subscriber) + await assertNilAsync(await subject.stateAdapter.statsReporter) + await assertNilAsync(await subject.stateAdapter.sfuAdapter) + await assertEqualAsync(await subject.stateAdapter.token, "") + await assertEqualAsync(await subject.stateAdapter.sessionID, "") + await assertEqualAsync(await subject.stateAdapter.ownCapabilities, []) + await assertEqualAsync(await subject.stateAdapter.participants, [:]) + await assertEqualAsync(await subject.stateAdapter.participantsCount, 0) + await assertEqualAsync(await subject.stateAdapter.anonymousCount, 0) + await assertEqualAsync(await subject.stateAdapter.participantPins, []) + } + + // MARK: - leave + + func test_leave_shouldTransitionStateMachineToLeaving() async throws { + mockWebRTCAuthenticator + .stub( + for: .authenticate, + with: Result<(SFUAdapter, JoinCallResponse), Error> + .success((mockSFUStack.adapter, JoinCallResponse.dummy())) + ) + mockWebRTCAuthenticator.stub( + for: .waitForAuthentication, + with: Result.success(()) + ) + + try await assertTransitionToStage( + .connected, + operation: { + try await self + .subject + .connect( + callSettings: nil, + options: nil, + ring: true, + notify: true + ) + } + ) { _ in + await self.assertTransitionToStage(.leaving) { + self.subject.leave() + } handler: { _ in } + } + } + + // MARK: - changeCameraMode + + func test_changeCameraMode_shouldUpdateCameraPosition() async throws { + try await prepareAsConnected() + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + + try await subject.changeCameraMode(position: .back) + + await assertEqualAsync( + await subject.stateAdapter.callSettings.cameraPosition, + .back + ) + XCTAssertEqual( + mockPublisher.recordedInputPayload( + AVCaptureDevice.Position.self, + for: .didUpdateCameraPosition + )?.first, + .back + ) + } + + // MARK: - changeAudioState + + func test_changeAudioState_shouldUpdateAudioState() async throws { + await subject.changeAudioState(isEnabled: false) + + await assertEqualAsync( + await subject.stateAdapter.callSettings.audioOn, + false + ) + } + + // MARK: - changeSoundState + + func test_changeSoundState_shouldUpdateAudioOutputState() async throws { + await subject.changeSoundState(isEnabled: false) + + await assertEqualAsync( + await subject.stateAdapter.callSettings.audioOutputOn, + false + ) + } + + // MARK: - changeSpeakerState + + func test_changeSpeakerState_shouldUpdateSpeakerState() async throws { + await subject.changeSpeakerState(isEnabled: false) + + await assertEqualAsync( + await subject.stateAdapter.callSettings.speakerOn, + false + ) + } + + // MARK: - changeTrackVisibility + + func test_changeTrackVisibility_shouldUpdateParticipantTrackVisibility() async throws { + try await prepareAsConnected() + + await subject.changeTrackVisibility( + for: .dummy(id: user.id), + isVisible: true + ) + + await fulfillment { + await self + .subject + .stateAdapter + .participants[self.user.id]? + .showTrack == true + } + } + + // MARK: - updateTrackSize + + func test_updateTrackSize_shouldUpdateParticipantTrackSize() async throws { + try await prepareAsConnected() + + await subject.updateTrackSize( + .init(width: 100, height: 200), + for: .dummy(id: user.id) + ) + + await fulfillment { + await self + .subject + .stateAdapter + .participants[self.user.id]? + .trackSize == .init(width: 100, height: 200) + } + } + + // MARK: - setVideoFilter + + func test_setVideoFilter_shouldSetVideoFilter() async throws { + let expected = VideoFilter(id: .unique, name: .unique, filter: { _ in fatalError() }) + try await prepareAsConnected(videoFilter: nil) + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + + await subject.setVideoFilter(expected) + + let actual = try XCTUnwrap(mockPublisher.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.last) + XCTAssertEqual(actual.id, expected.id) + XCTAssertEqual(actual.name, expected.name) + } + + // MARK: - startScreensharing + + func test_startScreensharing_typeIsInApp_shouldBeginScreenSharing() async throws { + try await prepareAsConnected() + let ownCapabilities = [OwnCapability.createReaction] + await subject.stateAdapter.set(ownCapabilities: Set(ownCapabilities)) + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + + try await subject.startScreensharing(type: .inApp) + + let actual = try XCTUnwrap( + mockPublisher.recordedInputPayload( + (ScreensharingType, [OwnCapability]).self, + for: .beginScreenSharing + )?.first + ) + XCTAssertEqual(actual.0, .inApp) + XCTAssertEqual(actual.1, ownCapabilities) + } + + func test_startScreensharing_typeIsBroadcast_shouldBeginScreenSharing() async throws { + try await prepareAsConnected() + let ownCapabilities = [OwnCapability.createReaction] + await subject.stateAdapter.set(ownCapabilities: Set(ownCapabilities)) + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + + try await subject.startScreensharing(type: .broadcast) + + let actual = try XCTUnwrap( + mockPublisher.recordedInputPayload( + (ScreensharingType, [OwnCapability]).self, + for: .beginScreenSharing + )?.first + ) + XCTAssertEqual(actual.0, .broadcast) + XCTAssertEqual(actual.1, ownCapabilities) + } + + // MARK: - stopScreensharing + + func test_stopScreensharing_shouldStopScreenSharing() async throws { + try await prepareAsConnected() + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + + try await subject.stopScreensharing() + + XCTAssertEqual(mockPublisher.timesCalled(.stopScreenSharing), 1) + } + + // MARK: - changePinState + + func test_changePinState_isEnabledTrue_shouldUpdateParticipantPin() async throws { + try await prepareAsConnected() + + try await subject.changePinState(isEnabled: true, sessionId: user.id) + + await fulfillment { + await self + .subject + .stateAdapter + .participants[self.user.id]? + .pin?.isLocal == true + } + } + + func test_changePinState_isEnabledFalse_shouldUpdateParticipantPin() async throws { + try await prepareAsConnected() + try await subject.changePinState(isEnabled: true, sessionId: user.id) + + try await subject.changePinState(isEnabled: false, sessionId: user.id) + + await fulfillment { + await self + .subject + .stateAdapter + .participants[self.user.id]? + .pin == nil + } + } + + // MARK: - startNoiseCancellation + + func test_startNoiseCancellation_shouldEnableNoiseCancellationForSession() async throws { + try await prepareAsConnected() + + try await subject.startNoiseCancellation(user.id) + + XCTAssertEqual( + mockSFUStack.service.startNoiseCancellationWasCalledWithRequest?.sessionID, + user.id + ) + } + + // MARK: - stopNoiseCancellation + + func test_stopNoiseCancellation_shouldDisableNoiseCancellationForSession() async throws { + try await prepareAsConnected() + + try await subject.stopNoiseCancellation(user.id) + + XCTAssertEqual( + mockSFUStack.service.stopNoiseCancellationWasCalledWithRequest?.sessionID, + user.id + ) + } + + // MARK: - focus + + func test_focus_shouldFocusOnSpecifiedPoint() async throws { + try await prepareAsConnected() + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + + try await subject.focus(at: .init(x: 10, y: 20)) + + XCTAssertEqual( + mockPublisher.recordedInputPayload(CGPoint.self, for: .focus)?.first, + .init(x: 10, y: 20) + ) + } + + // MARK: - addCapturePhotoOutput + + func test_addCapturePhotoOutput_shouldAddPhotoOutputToCaptureSession() async throws { + try await prepareAsConnected() + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + let expected = AVCapturePhotoOutput() + + try await subject.addCapturePhotoOutput(expected) + + XCTAssertTrue( + mockPublisher.recordedInputPayload(AVCapturePhotoOutput.self, for: .addCapturePhotoOutput)?.first === expected + ) + } + + // MARK: - removeCapturePhotoOutput + + func test_removeCapturePhotoOutput_shouldRemovePhotoOutputFromCaptureSession() async throws { + try await prepareAsConnected() + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + let expected = AVCapturePhotoOutput() + + try await subject.removeCapturePhotoOutput(expected) + + XCTAssertTrue( + mockPublisher.recordedInputPayload(AVCapturePhotoOutput.self, for: .removeCapturePhotoOutput)?.first === expected + ) + } + + // MARK: - addVideoOutput + + func test_addVideoOutput_shouldAddVideoOutputToCaptureSession() async throws { + try await prepareAsConnected() + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + let expected = AVCaptureVideoDataOutput() + + try await subject.addVideoOutput(expected) + + XCTAssertTrue( + mockPublisher.recordedInputPayload(AVCaptureVideoDataOutput.self, for: .addVideoOutput)?.first === expected + ) + } + + // MARK: - removeVideoOutput + + func test_removeVideoOutput_shouldRemoveVideoOutputFromCaptureSession() async throws { + try await prepareAsConnected() + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + let expected = AVCaptureVideoDataOutput() + + try await subject.removeVideoOutput(expected) + + XCTAssertTrue( + mockPublisher.recordedInputPayload(AVCaptureVideoDataOutput.self, for: .removeVideoOutput)?.first === expected + ) + } + + // MARK: - zoom + + func test_zoom_shouldZoomCameraBySpecifiedFactor() async throws { + try await prepareAsConnected() + let mockPublisher = try await XCTAsyncUnwrap( + await subject + .stateAdapter + .publisher as? MockRTCPeerConnectionCoordinator + ) + + try await subject.zoom(by: 32) + + XCTAssertEqual( + mockPublisher.recordedInputPayload(CGFloat.self, for: .zoom)?.first, + 32 + ) + } + + // MARK: - setIncomingVideoQualitySettings + + func test_setIncomingVideoQualitySettings_correctlyUpdatesStateAdapter() async throws { + try await prepareAsConnected() + let incomingVideoQualitySettings = IncomingVideoQualitySettings.manual( + group: .custom(sessionIds: [.unique, .unique]), + targetSize: .init( + width: 11, + height: 10 + ) + ) + + await subject.setIncomingVideoQualitySettings(incomingVideoQualitySettings) + + await assertEqualAsync( + await subject.stateAdapter.incomingVideoQualitySettings, + incomingVideoQualitySettings + ) + } + + // MARK: - setDisconnectionTimeout + + func test_setDisconnectionTimeout_correctlyUpdatesStageContext() async throws { + try await prepareAsConnected() + + subject.setDisconnectionTimeout(11) + + XCTAssertEqual( + subject.stateMachine.currentStage.context.disconnectionTimeout, + 11 + ) + } + + // MARK: - updatePublishOptions + + func test_updatePublishOptions_shouldCallUpdatePublishOptionsCoordinator() async throws { + try await prepareAsConnected() + + await subject.updatePublishOptions( + preferredVideoCodec: .vp9, + maxBitrate: 1000 + ) + + let publishOptions = await subject + .stateAdapter + .publishOptions + XCTAssertEqual(publishOptions.video.count, 1) + let videoPublishOptions = try XCTUnwrap(publishOptions.video.first) + XCTAssertEqual(videoPublishOptions.codec, .vp9) + XCTAssertEqual(videoPublishOptions.bitrate, 1000) + } + + // MARK: - Private helpers + + private func assertEqualAsync( + _ expression: @autoclosure () async throws -> T, + _ expected: @autoclosure () async throws -> T, + file: StaticString = #file, + line: UInt = #line + ) async rethrows { + let value = try await expression() + let expectedValue = try await expected() + XCTAssertEqual(value, expectedValue, file: file, line: line) + } + + private func assertNoThrowAsync( + _ expression: @autoclosure () async throws -> Void, + file: StaticString = #file, + line: UInt = #line + ) async { + do { + try await expression() + } catch { + let thrower = { throw error } + XCTAssertNoThrow(try thrower(), file: file, line: line) + } + } + + private func assertTransitionToStage( + _ id: WebRTCCoordinator.StateMachine.Stage.ID, + operation: @escaping () async throws -> Void, + handler: @escaping (WebRTCCoordinator.StateMachine.Stage) async throws -> Void, + file: StaticString = #file, + line: UInt = #line + ) async rethrows { + let transitionExpectation = expectation(description: "WebRTCCoordinator is expected to transition to stage id:\(id).") + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let target = try await self + .subject + .stateMachine + .publisher + .filter { $0.id == id } + .nextValue(timeout: defaultTimeout) + + await self.assertNoThrowAsync( + try await handler(target), + file: file, + line: line + ) + transitionExpectation.fulfill() + } + group.addTask { + await self.wait(for: 0.1) + try await operation() + } + group.addTask { + await self.fulfillment(of: [transitionExpectation], timeout: defaultTimeout) + } + + try await group.waitForAll() + } + } + + private func assertNilAsync( + _ expression: @autoclosure () async throws -> T?, + file: StaticString = #file, + line: UInt = #line + ) async rethrows { + let value = try await expression() + XCTAssertNil(value, file: file, line: line) + } + + private func prepareAsConnected( + videoFilter: VideoFilter? = VideoFilter( + id: .unique, + name: .unique, + filter: { _ in fatalError() } + ) + ) async throws { + mockSFUStack.setConnectionState(to: .connected(healthCheckInfo: .init())) + let ownCapabilities = Set([OwnCapability.blockUsers, .changeMaxDuration]) + let callSettings = CallSettings(cameraPosition: .back) + await subject.stateAdapter.set(sfuAdapter: mockSFUStack.adapter) + if let videoFilter { + await subject.stateAdapter.set(videoFilter: videoFilter) + } + await subject.stateAdapter.set(ownCapabilities: ownCapabilities) + await subject.stateAdapter.set(callSettings: callSettings) + await subject.stateAdapter.set(sessionID: .unique) + await subject.stateAdapter.set(token: .unique) + await subject.stateAdapter.set(participantsCount: 12) + await subject.stateAdapter.set(anonymousCount: 22) + await subject.stateAdapter.set(participantPins: [PinInfo(isLocal: true, pinnedAt: .init())]) + await subject.stateAdapter.enqueue { _ in [self.user.id: CallParticipant.dummy(id: self.user.id)] } + try await subject.stateAdapter.configurePeerConnections() + await subject.stateAdapter.set(statsReporter: WebRTCStatsReporter(sessionID: .unique)) + } +} diff --git a/StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift b/StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift index 7e272c686..e5cba2763 100644 --- a/StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift +++ b/StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift @@ -6,725 +6,759 @@ import StreamWebRTC @preconcurrency import XCTest -// final class WebRTCStateAdapter_Tests: XCTestCase, @unchecked Sendable { -// -// private lazy var user: User! = .dummy() -// private lazy var apiKey: String! = .unique -// private lazy var callCid: String! = .unique -// private lazy var videoConfig: VideoConfig! = .dummy() -// private lazy var rtcPeerConnectionCoordinatorFactory: MockRTCPeerConnectionCoordinatorFactory! = .init() -// private lazy var screenShareSessionProvider: ScreenShareSessionProvider! = .init() -// private lazy var subject: WebRTCStateAdapter! = .init( -// user: user, -// apiKey: apiKey, -// callCid: callCid, -// videoConfig: videoConfig, -// rtcPeerConnectionCoordinatorFactory: rtcPeerConnectionCoordinatorFactory, -// screenShareSessionProvider: screenShareSessionProvider -// ) -// -// // MARK: - Lifecycle -// -// override func tearDown() { -// subject = nil -// screenShareSessionProvider = nil -// rtcPeerConnectionCoordinatorFactory = nil -// videoConfig = nil -// callCid = nil -// apiKey = nil -// user = nil -// super.tearDown() -// } -// -// // MARK: - audioSession -// -// func test_audioSession_delegateWasSetAsExpected() async throws { -// await assertTrueAsync(await subject.audioSession.delegate === subject) -// } -// -// // MARK: - setSessionID -// -// func test_sessionID_shouldNotBeEmptyOnInit() async throws { -// await assertEqualAsync(await subject.sessionID.isEmpty, false) -// } -// -// func test_setSessionID_shouldUpdateSessionID() async throws { -// _ = subject -// _ = try await subject -// .$sessionID -// .filter { !$0.isEmpty } -// .nextValue() -// let expected = String.unique -// -// await subject.set(sessionID: expected) -// -// await assertEqualAsync(await subject.sessionID, expected) -// } -// -// // MARK: - setCallSettings -// -// func test_setCallSettings_shouldUpdateCallSettings() async throws { -// let expected = CallSettings(cameraPosition: .back) -// -// await subject.set(callSettings: expected) -// -// await assertEqualAsync(await subject.callSettings, expected) -// } -// -// // MARK: - setInitialCallSettings -// -// func test_setInitialCallSettings_shouldUpdateInitialCallSettings() async throws { -// let expected = CallSettings(cameraPosition: .back) -// -// await subject.set(initialCallSettings: expected) -// -// await assertEqualAsync(await subject.initialCallSettings, expected) -// } -// -// // MARK: - setAudioSettings -// -// func test_setAudioSettings_shouldUpdateAudioSettings() async throws { -// let expected = AudioSettings( -// accessRequestEnabled: false, -// defaultDevice: .speaker, -// micDefaultOn: true, -// opusDtxEnabled: true, -// redundantCodingEnabled: true, -// speakerDefaultOn: true -// ) -// -// await subject.set(audioSettings: expected) -// -// await assertEqualAsync(await subject.audioSettings, expected) -// } -// -// // MARK: - setVideoOptions -// -// func test_setVideoOptions_shouldUpdateVideoOptions() async throws { -// let expected = VideoOptions( -// preferredTargetResolution: .dummy(bitrate: 10000, height: 200, width: 300), -// preferredFormat: nil, -// preferredFps: 109_999 -// ) -// -// await subject.set(videoOptions: expected) -// -// await assertEqualAsync(await subject.videoOptions.preferredDimensions.width, expected.preferredDimensions.width) -// await assertEqualAsync(await subject.videoOptions.preferredDimensions.height, expected.preferredDimensions.height) -// await assertEqualAsync(await subject.videoOptions.preferredFps, expected.preferredFps) -// } -// -// // MARK: - setConnectOptions -// -// func test_setConnectOptions_shouldUpdateConnectOptions() async throws { -// let expected = ConnectOptions( -// iceServers: [ -// .init(password: .unique, urls: [.unique], username: .unique) -// ] -// ) -// -// await subject.set(connectOptions: expected) -// -// await assertEqualAsync( -// await subject.connectOptions.rtcConfiguration.iceServers.map { -// ICEServer( -// password: $0.credential ?? .unique, -// urls: $0.urlStrings, -// username: $0.username ?? .unique -// ) -// }, -// expected.rtcConfiguration.iceServers.map { -// ICEServer( -// password: $0.credential ?? .unique, -// urls: $0.urlStrings, -// username: $0.username ?? .unique -// ) -// } -// ) -// } -// -// // MARK: - setOwnCapabilities -// -// func test_setOwnCapabilities_shouldUpdateOwnCapabilities() async throws { -// let expected = Set([OwnCapability.blockUsers, .removeCallMember]) -// -// await subject.set(ownCapabilities: expected) -// -// await assertEqualAsync(await subject.ownCapabilities, expected) -// } -// -// // MARK: - setStatsReporter -// -// func test_setStatsReporter_shouldUpdateStatsReporter() async throws { -// let expected = WebRTCStatsReporter(sessionID: .unique) -// -// await subject.set(statsReporter: expected) -// -// await assertTrueAsync(await subject.statsReporter === expected) -// } -// -// // MARK: - setSFUAdapter -// -// func test_setSFUAdapter_shouldUpdateSFUAdapterAndStatsReporter() async throws { -// let statsReporter = WebRTCStatsReporter(sessionID: .unique) -// await subject.set(statsReporter: statsReporter) -// let mockSFUStack = MockSFUStack() -// -// await subject.set(sfuAdapter: mockSFUStack.adapter) -// -// await assertTrueAsync(await subject.sfuAdapter === mockSFUStack.adapter) -// XCTAssertTrue(statsReporter.sfuAdapter === mockSFUStack.adapter) -// } -// -// // MARK: - setParticipantsCount -// -// func test_setParticipantsCount_shouldUpdateParticipantsCount() async throws { -// let expected = UInt32(32) -// -// await subject.set(participantsCount: expected) -// -// await assertEqualAsync(await subject.participantsCount, expected) -// } -// -// // MARK: - setAnonymousCount -// -// func test_setAnonymousCount_shouldUpdateAnonymousCount() async throws { -// let expected = UInt32(32) -// -// await subject.set(anonymousCount: expected) -// -// await assertEqualAsync(await subject.anonymousCount, expected) -// } -// -// // MARK: - setParticipantPins -// -// func test_setParticipantPins_shouldUpdateParticipantPins() async throws { -// let expected = [PinInfo(isLocal: true, pinnedAt: .init(timeIntervalSince1970: 100))] -// -// await subject.set(participantPins: expected) -// -// await assertEqualAsync(await subject.participantPins, expected) -// } -// -// // MARK: - setToken -// -// func test_setToken_shouldUpdateToken() async throws { -// let expected = String.unique -// -// await subject.set(token: expected) -// -// await assertEqualAsync(await subject.token, expected) -// } -// -// // MARK: - setIncomingVideoQualitySettings -// -// func test_setIncomingVideoQualitySettings_shouldUpdateIncomingVideoQualitySettings() async throws { -// let expected = IncomingVideoQualitySettings.manual( -// group: .custom(sessionIds: [.unique, .unique]), -// targetSize: .init( -// width: 11, -// height: 10 -// ) -// ) -// -// await subject.set(incomingVideoQualitySettings: expected) -// -// await assertEqualAsync(await subject.incomingVideoQualitySettings, expected) -// } -// -// // MARK: - setVideoFilter -// -// func test_setVideoFilter_shouldUpdateVideoFilter() async throws { -// let sfuStack = MockSFUStack() -// await subject.set(sfuAdapter: sfuStack.adapter) -// try await subject.configurePeerConnections() -// let expected = VideoFilter(id: .unique, name: .unique, filter: { _ in fatalError() }) -// -// await subject.set(videoFilter: expected) -// -// let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) -// XCTAssertEqual( -// mockPublisher.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.first?.id, -// expected.id -// ) -// XCTAssertEqual( -// mockPublisher.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.first?.name, -// expected.name -// ) -// } -// -// // MARK: - refreshSession -// -// func test_refreshSession_shouldUpdateSessionID() async throws { -// let currentSessionId = await subject.sessionID -// -// await subject.refreshSession() -// -// let newSessionId = await subject.sessionID -// XCTAssertNotEqual(newSessionId, currentSessionId) -// } -// -// // MARK: - configurePeerConnections -// -// func test_configurePeerConnections_withSFU_shouldSetupPeerConnections() async throws { -// let sfuStack = MockSFUStack() -// await subject.set(sfuAdapter: sfuStack.adapter) -// let videoFilter = VideoFilter( -// id: .unique, -// name: .unique, -// filter: { _ in fatalError() } -// ) -// await subject.set(videoFilter: videoFilter) -// let ownCapabilities = Set([OwnCapability.blockUsers, .changeMaxDuration]) -// await subject.set(ownCapabilities: ownCapabilities) -// let callSettings = CallSettings(cameraPosition: .back) -// await subject.set(callSettings: callSettings) -// -// try await subject.configurePeerConnections() -// -// let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) -// let mockSubscriber = try await XCTAsyncUnwrap(await subject.subscriber as? MockRTCPeerConnectionCoordinator) -// -// XCTAssertEqual( -// mockPublisher.recordedInputPayload( -// (CallSettings, [OwnCapability]).self, -// for: .setUp -// )?.first?.0, -// callSettings -// ) -// XCTAssertEqual( -// mockPublisher.recordedInputPayload( -// (CallSettings, [OwnCapability]).self, -// for: .setUp -// )?.first?.1.sorted(), -// Array(ownCapabilities).sorted() -// ) -// XCTAssertEqual( -// mockPublisher.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.first?.id, -// videoFilter.id -// ) -// XCTAssertEqual( -// mockPublisher.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.first?.name, -// videoFilter.name -// ) -// -// XCTAssertEqual( -// mockSubscriber.recordedInputPayload( -// (CallSettings, [OwnCapability]).self, -// for: .setUp -// )?.first?.0, -// callSettings -// ) -// XCTAssertEqual( -// mockSubscriber.recordedInputPayload( -// (CallSettings, [OwnCapability]).self, -// for: .setUp -// )?.first?.1.sorted(), -// Array(ownCapabilities).sorted() -// ) -// } -// -// // MARK: - cleanUp -// -// func test_cleanUp_shouldResetProperties() async throws { -// let sfuStack = MockSFUStack() -// try await prepare(sfuStack: sfuStack) -// let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) -// let mockSubscriber = try await XCTAsyncUnwrap(await subject.subscriber as? MockRTCPeerConnectionCoordinator) -// -// await subject.cleanUp() -// -// XCTAssertEqual(mockPublisher.timesCalled(.close), 1) -// XCTAssertEqual(mockSubscriber.timesCalled(.close), 1) -// XCTAssertEqual(sfuStack.webSocket.timesCalled(.disconnectAsync), 1) -// -// await fulfillment { await self.subject.publisher == nil } -// await assertNilAsync(await subject.publisher) -// await assertNilAsync(await subject.subscriber) -// await assertNilAsync(await subject.statsReporter) -// await assertNilAsync(await subject.sfuAdapter) -// await assertEqualAsync(await subject.token, "") -// await assertEqualAsync(await subject.sessionID, "") -// await assertEqualAsync(await subject.ownCapabilities, []) -// await assertEqualAsync(await subject.participants, [:]) -// await assertEqualAsync(await subject.participantsCount, 0) -// await assertEqualAsync(await subject.anonymousCount, 0) -// await assertEqualAsync(await subject.participantPins, []) -// } -// -// // MARK: - cleanUpForReconnection -// -// func test_cleanUpForReconnection_shouldResetPropertiesForReconnection() async throws { -// let sfuStack = MockSFUStack() -// let ownCapabilities = Set([OwnCapability.blockUsers]) -// let pins = [PinInfo(isLocal: true, pinnedAt: .init())] -// let userId = String.unique -// let participants = [userId: CallParticipant.dummy(id: userId)] -// try await prepare( -// sfuStack: sfuStack, -// ownCapabilities: ownCapabilities, -// participants: participants, -// participantPins: pins -// ) -// let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) -// let mockSubscriber = try await XCTAsyncUnwrap(await subject.subscriber as? MockRTCPeerConnectionCoordinator) -// let sessionId = await subject.sessionID -// await subject.didAddTrack( -// .dummy( -// kind: .video, -// peerConnectionFactory: await subject.peerConnectionFactory -// ), -// type: .video, -// for: userId -// ) -// await fulfillment { await self.subject.participants[userId]?.track != nil } -// await subject.cleanUpForReconnection() -// -// XCTAssertEqual(mockPublisher.timesCalled(.close), 0) -// XCTAssertEqual(mockSubscriber.timesCalled(.close), 0) -// XCTAssertEqual(sfuStack.webSocket.timesCalled(.disconnectAsync), 0) -// await assertNilAsync(await subject.publisher) -// await assertNilAsync(await subject.subscriber) -// await assertNilAsync(await subject.statsReporter) -// await assertNilAsync(await subject.sfuAdapter) -// await assertEqualAsync(await subject.token, "") -// await assertEqualAsync(await subject.sessionID, sessionId) -// await assertEqualAsync(await subject.ownCapabilities, ownCapabilities) -// await assertEqualAsync(await subject.participants[userId]?.track, nil) -// await assertEqualAsync(await subject.participantsCount, 12) -// await assertEqualAsync(await subject.anonymousCount, 22) -// await assertEqualAsync(await subject.participantPins, pins) -// } -// -// // MARK: - restoreScreenSharing -// -// func test_restoreScreenSharing_withActiveSession_shouldBeginScreenSharing() async throws { -// let sfuStack = MockSFUStack() -// sfuStack.setConnectionState(to: .connected(healthCheckInfo: .init())) -// await subject.set(sfuAdapter: sfuStack.adapter) -// try await subject.configurePeerConnections() -// let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) -// screenShareSessionProvider.activeSession = .init( -// localTrack: await subject.peerConnectionFactory.mockVideoTrack(forScreenShare: true), -// screenSharingType: .inApp, -// capturer: MockVideoCapturer() -// ) -// let ownCapabilities = Set([OwnCapability.blockUsers]) -// await subject.set(ownCapabilities: ownCapabilities) -// -// try await subject.restoreScreenSharing() -// -// XCTAssertEqual( -// mockPublisher.recordedInputPayload( -// (ScreensharingType, [OwnCapability]).self, -// for: .beginScreenSharing -// )?.first?.0, -// .inApp -// ) -// XCTAssertEqual( -// mockPublisher.recordedInputPayload( -// (ScreensharingType, [OwnCapability]).self, -// for: .beginScreenSharing -// )?.first?.1, -// [.blockUsers] -// ) -// } -// -// func test_restoreScreenSharing_withoutActiveSession_shouldNotBeginScreenSharing() async throws { -// let sfuStack = MockSFUStack() -// sfuStack.setConnectionState(to: .connected(healthCheckInfo: .init())) -// await subject.set(sfuAdapter: sfuStack.adapter) -// try await subject.configurePeerConnections() -// let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) -// let ownCapabilities = Set([OwnCapability.blockUsers]) -// await subject.set(ownCapabilities: ownCapabilities) -// -// try await subject.restoreScreenSharing() -// -// XCTAssertEqual(mockPublisher.timesCalled(.beginScreenSharing), 0) -// } -// -// // MARK: - didAddTrack -// -// func test_didAddTrack_videoOfExistingParticipant_shouldAddTrack() async throws { -// let participant = CallParticipant.dummy() -// let track = await subject -// .peerConnectionFactory -// .mockVideoTrack(forScreenShare: false) -// await subject.enqueue { _ in [participant.sessionId: participant] } -// -// await subject.didAddTrack( -// track, -// type: .video, -// for: participant.sessionId -// ) -// -// await fulfillment { -// await self -// .subject -// .participants[participant.sessionId]? -// .track? -// .trackId == track.trackId -// } -// } -// -// func test_didAddTrack_screenSharingOfExistingParticipant_shouldAddTrack() async throws { -// let participant = CallParticipant.dummy() -// let track = await subject -// .peerConnectionFactory -// .mockVideoTrack(forScreenShare: true) -// await subject.enqueue { _ in [participant.sessionId: participant] } -// -// await subject.didAddTrack( -// track, -// type: .screenshare, -// for: participant.sessionId -// ) -// -// await fulfillment { -// await self -// .subject -// .participants[participant.sessionId]? -// .screenshareTrack? -// .trackId == track.trackId -// } -// } -// -// // MARK: - didRemoveTrack -// -// func test_didRemoveTrack_videoOfExistingParticipant_shouldRemoveTrack() async throws { -// let participant = CallParticipant.dummy() -// let track = await subject -// .peerConnectionFactory -// .mockVideoTrack(forScreenShare: false) -// await subject.enqueue { _ in [participant.sessionId: participant] } -// await subject.didAddTrack(track, type: .video, for: participant.sessionId) -// -// await subject.didRemoveTrack( -// for: participant.sessionId -// ) -// -// await fulfillment { -// await self -// .subject -// .participants[participant.sessionId]? -// .track? -// .trackId == nil -// } -// } -// -// func test_didRemoveTrack_screenSharingOfExistingParticipant_shouldRemoveTrack() async throws { -// let participant = CallParticipant.dummy() -// let track = await subject -// .peerConnectionFactory -// .mockVideoTrack(forScreenShare: true) -// await subject.enqueue { _ in [participant.sessionId: participant] } -// await subject.didAddTrack(track, type: .screenshare, for: participant.sessionId) -// -// await subject.didRemoveTrack( -// for: participant.sessionId -// ) -// -// await fulfillment { -// await self -// .subject -// .participants[participant.sessionId]? -// .screenshareTrack? -// .trackId == nil -// } -// } -// -// // MARK: - trackFor -// -// func test_trackFor_withVideo_shouldReturnCorrectTrack() async throws { -// let participant = CallParticipant.dummy() -// let track = await subject -// .peerConnectionFactory -// .mockVideoTrack(forScreenShare: false) -// await subject.enqueue { _ in [participant.sessionId: participant] } -// await subject.didAddTrack(track, type: .video, for: participant.sessionId) -// -// let actual = await subject.track(for: participant.sessionId, of: .video) -// -// XCTAssertEqual(track.trackId, actual?.trackId) -// } -// -// func test_trackFor_withScreenShare_shouldReturnCorrectTrack() async throws { -// let participant = CallParticipant.dummy() -// let track = await subject -// .peerConnectionFactory -// .mockVideoTrack(forScreenShare: true) -// await subject.enqueue { _ in [participant.sessionId: participant] } -// await subject.didAddTrack(track, type: .screenshare, for: participant.sessionId) -// -// let actual = await subject.track(for: participant.sessionId, of: .screenshare) -// -// XCTAssertEqual(track.trackId, actual?.trackId) -// } -// -// // MARK: - didUpdateVideoOptions -// -// func test_didUpdateVideoOptions_shouldUpdateVideoOptions() async throws { -// let sfuStack = MockSFUStack() -// sfuStack.setConnectionState(to: .connected(healthCheckInfo: .init())) -// await subject.set(sfuAdapter: sfuStack.adapter) -// try await subject.configurePeerConnections() -// let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) -// let mockSubscriber = try await XCTAsyncUnwrap(await subject.subscriber as? MockRTCPeerConnectionCoordinator) -// let newVideoOptions = VideoOptions( -// preferredTargetResolution: .dummy(), -// preferredFormat: nil, -// preferredFps: 17 -// ) -// -// await subject.set(videoOptions: newVideoOptions) -// -// XCTAssertEqual(mockPublisher.videoOptions.preferredFps, 17) -// XCTAssertEqual(mockSubscriber.videoOptions.preferredFps, 17) -// } -// -// // MARK: - didUpdateParticipants -// -// func test_didUpdateParticipants_shouldAssignTracksToParticipants() async throws { -// let initialParticipants: [String: CallParticipant] = [ -// "1": .dummy(id: "1"), -// "2": .dummy(id: "2"), -// "3": .dummy(id: "3") -// ] -// let participantTracks: [String: RTCMediaStreamTrack] = [ -// "1": await subject.peerConnectionFactory.mockAudioTrack(), -// "2": await subject.peerConnectionFactory.mockVideoTrack(forScreenShare: false), -// "3": await subject.peerConnectionFactory.mockVideoTrack(forScreenShare: true) -// ] -// await subject.enqueue { _ in initialParticipants } -// -// await subject.didAddTrack(participantTracks["2"]!, type: .video, for: "2") -// await subject.didAddTrack(participantTracks["3"]!, type: .screenshare, for: "3") -// -// await subject.enqueue { _ in initialParticipants } -// -// await fulfillment { -// let participant2 = await self.subject.participants["2"] -// let participant3 = await self.subject.participants["3"] -// -// return participant2?.track?.trackId == participantTracks["2"]?.trackId -// && participant3?.screenshareTrack?.trackId == participantTracks["3"]?.trackId -// } -// } -// -// func test_didUpdateParticipants_withIncomingVideoQualitySettings_shouldAssignTracksToParticipantsCorrectly() async throws { -// let initialParticipants: [String: CallParticipant] = [ -// "1": .dummy(id: "1"), -// "2": .dummy(id: "2"), -// "3": .dummy(id: "3") -// ] -// let participantTracks: [String: RTCMediaStreamTrack] = [ -// "1": await subject.peerConnectionFactory.mockAudioTrack(), -// "2": await subject.peerConnectionFactory.mockVideoTrack(forScreenShare: false), -// "3": await subject.peerConnectionFactory.mockVideoTrack(forScreenShare: true) -// ] -// await subject.set(incomingVideoQualitySettings: .disabled(group: .custom(sessionIds: ["2"]))) -// await subject.enqueue { _ in initialParticipants } -// -// await subject.didAddTrack(participantTracks["2"]!, type: .video, for: "2") -// await subject.didAddTrack(participantTracks["3"]!, type: .screenshare, for: "3") -// -// await subject.enqueue { _ in initialParticipants } -// -// await fulfillment { -// let participant2 = await self.subject.participants["2"] -// let participant3 = await self.subject.participants["3"] -// -// return participant2?.track == nil -// && participant3?.screenshareTrack?.trackId == participantTracks["3"]?.trackId -// } -// } -// -// // MARK: - audioSessionDidUpdateCallSettings -// -// func test_audioSessionDidUpdateCallSettings_updatesCallSettingsAsExpected() async { -// let updatedCallSettings = CallSettings( -// audioOn: false, -// videoOn: false, -// speakerOn: true, -// audioOutputOn: false, -// cameraPosition: .back -// ) -// -// subject.audioSessionAdapterDidUpdateCallSettings( -// await subject.audioSession, -// callSettings: updatedCallSettings -// ) -// -// await fulfillment { [subject] in -// await subject?.callSettings == updatedCallSettings -// } -// } -// -// // MARK: - Private helpers -// -// private func assertNilAsync( -// _ expression: @autoclosure () async throws -> T?, -// file: StaticString = #file, -// line: UInt = #line -// ) async rethrows { -// let value = try await expression() -// XCTAssertNil(value, file: file, line: line) -// } -// -// private func assertEqualAsync( -// _ expression: @autoclosure () async throws -> T, -// _ expected: @autoclosure () async throws -> T, -// file: StaticString = #file, -// line: UInt = #line -// ) async rethrows { -// let value = try await expression() -// let expectedValue = try await expected() -// XCTAssertEqual(value, expectedValue, file: file, line: line) -// } -// -// private func assertTrueAsync( -// _ expression: @autoclosure () async throws -> Bool, -// file: StaticString = #file, -// line: UInt = #line -// ) async rethrows { -// let value = try await expression() -// XCTAssertTrue(value, file: file, line: line) -// } -// -// private func prepare( -// sfuStack: MockSFUStack = .init(), -// ownCapabilities: Set = [.changeMaxDuration], -// participants: [String: CallParticipant] = [.unique: .dummy()], -// participantPins: [PinInfo] = [.init(isLocal: true, pinnedAt: .init())] -// ) async throws { -// sfuStack.setConnectionState(to: .connected(healthCheckInfo: .init())) -// let callSettings = CallSettings(cameraPosition: .back) -// let videoFilter = VideoFilter( -// id: .unique, -// name: .unique, -// filter: { _ in fatalError() } -// ) -// await fulfillment { await self.subject.sessionID.isEmpty == false } -// await subject.set(sfuAdapter: sfuStack.adapter) -// await subject.set(videoFilter: videoFilter) -// await subject.set(ownCapabilities: ownCapabilities) -// await subject.set(callSettings: callSettings) -// await subject.set(token: .unique) -// await subject.set(participantsCount: 12) -// await subject.set(anonymousCount: 22) -// await subject.set(participantPins: participantPins) -// await subject.enqueue { _ in participants } -// try await subject.configurePeerConnections() -// await subject.set(statsReporter: WebRTCStatsReporter(sessionID: .unique)) -// } -// } -// -// extension OwnCapability: Comparable { -// public static func < ( -// lhs: OwnCapability, -// rhs: OwnCapability -// ) -> Bool { -// lhs.rawValue <= rhs.rawValue -// } -// } +final class WebRTCStateAdapter_Tests: XCTestCase, @unchecked Sendable { + + private lazy var user: User! = .dummy() + private lazy var apiKey: String! = .unique + private lazy var callCid: String! = .unique + private lazy var videoConfig: VideoConfig! = .dummy() + private lazy var rtcPeerConnectionCoordinatorFactory: MockRTCPeerConnectionCoordinatorFactory! = .init() + private lazy var screenShareSessionProvider: ScreenShareSessionProvider! = .init() + private lazy var subject: WebRTCStateAdapter! = .init( + user: user, + apiKey: apiKey, + callCid: callCid, + videoConfig: videoConfig, + rtcPeerConnectionCoordinatorFactory: rtcPeerConnectionCoordinatorFactory, + screenShareSessionProvider: screenShareSessionProvider + ) + + // MARK: - Lifecycle + + override func tearDown() { + subject = nil + screenShareSessionProvider = nil + rtcPeerConnectionCoordinatorFactory = nil + videoConfig = nil + callCid = nil + apiKey = nil + user = nil + super.tearDown() + } + + // MARK: - audioSession + + func test_audioSession_delegateWasSetAsExpected() async throws { + await assertTrueAsync(await subject.audioSession.delegate === subject) + } + + // MARK: - setSessionID + + func test_sessionID_shouldNotBeEmptyOnInit() async throws { + await assertEqualAsync(await subject.sessionID.isEmpty, false) + } + + func test_setSessionID_shouldUpdateSessionID() async throws { + _ = subject + _ = try await subject + .$sessionID + .filter { !$0.isEmpty } + .nextValue() + let expected = String.unique + + await subject.set(sessionID: expected) + + await assertEqualAsync(await subject.sessionID, expected) + } + + // MARK: - setCallSettings + + func test_setCallSettings_shouldUpdateCallSettings() async throws { + let expected = CallSettings(cameraPosition: .back) + + await subject.set(callSettings: expected) + + await assertEqualAsync(await subject.callSettings, expected) + } + + // MARK: - setInitialCallSettings + + func test_setInitialCallSettings_shouldUpdateInitialCallSettings() async throws { + let expected = CallSettings(cameraPosition: .back) + + await subject.set(initialCallSettings: expected) + + await assertEqualAsync(await subject.initialCallSettings, expected) + } + + // MARK: - setAudioSettings + + func test_setAudioSettings_shouldUpdateAudioSettings() async throws { + let expected = AudioSettings( + accessRequestEnabled: false, + defaultDevice: .speaker, + micDefaultOn: true, + opusDtxEnabled: true, + redundantCodingEnabled: true, + speakerDefaultOn: true + ) + + await subject.set(audioSettings: expected) + + await assertEqualAsync(await subject.audioSettings, expected) + } + + // MARK: - setVideoOptions + + func test_setVideoOptions_shouldUpdateVideoOptions() async throws { + let expected = VideoOptions( + preferredCameraPosition: .back + ) + + await subject.set(videoOptions: expected) + + await assertEqualAsync(await subject.videoOptions.preferredCameraPosition, expected.preferredCameraPosition) + } + + // MARK: - setPublishOptions + + func test_setPublishOptions_shouldUpdatePublishOptions() async throws { + let expected = PublishOptions( + video: [.dummy(codec: .av1)] + ) + + await subject.set(publishOptions: expected) + + await assertEqualAsync(await subject.publishOptions, expected) + } + + // MARK: - setConnectOptions + + func test_setConnectOptions_shouldUpdateConnectOptions() async throws { + let expected = ConnectOptions( + iceServers: [ + .init(password: .unique, urls: [.unique], username: .unique) + ] + ) + + await subject.set(connectOptions: expected) + + await assertEqualAsync( + await subject.connectOptions.rtcConfiguration.iceServers.map { + ICEServer( + password: $0.credential ?? .unique, + urls: $0.urlStrings, + username: $0.username ?? .unique + ) + }, + expected.rtcConfiguration.iceServers.map { + ICEServer( + password: $0.credential ?? .unique, + urls: $0.urlStrings, + username: $0.username ?? .unique + ) + } + ) + } + + // MARK: - setOwnCapabilities + + func test_setOwnCapabilities_shouldUpdateOwnCapabilities() async throws { + let expected = Set([OwnCapability.blockUsers, .removeCallMember]) + + await subject.set(ownCapabilities: expected) + + await assertEqualAsync(await subject.ownCapabilities, expected) + } + + // MARK: - setStatsReporter + + func test_setStatsReporter_shouldUpdateStatsReporter() async throws { + let expected = WebRTCStatsReporter(sessionID: .unique) + + await subject.set(statsReporter: expected) + + await assertTrueAsync(await subject.statsReporter === expected) + } + + // MARK: - setSFUAdapter + + func test_setSFUAdapter_shouldUpdateSFUAdapterAndStatsReporter() async throws { + let statsReporter = WebRTCStatsReporter(sessionID: .unique) + await subject.set(statsReporter: statsReporter) + let mockSFUStack = MockSFUStack() + + await subject.set(sfuAdapter: mockSFUStack.adapter) + + await assertTrueAsync(await subject.sfuAdapter === mockSFUStack.adapter) + XCTAssertTrue(statsReporter.sfuAdapter === mockSFUStack.adapter) + } + + // MARK: - setParticipantsCount + + func test_setParticipantsCount_shouldUpdateParticipantsCount() async throws { + let expected = UInt32(32) + + await subject.set(participantsCount: expected) + + await assertEqualAsync(await subject.participantsCount, expected) + } + + // MARK: - setAnonymousCount + + func test_setAnonymousCount_shouldUpdateAnonymousCount() async throws { + let expected = UInt32(32) + + await subject.set(anonymousCount: expected) + + await assertEqualAsync(await subject.anonymousCount, expected) + } + + // MARK: - setParticipantPins + + func test_setParticipantPins_shouldUpdateParticipantPins() async throws { + let expected = [PinInfo(isLocal: true, pinnedAt: .init(timeIntervalSince1970: 100))] + + await subject.set(participantPins: expected) + + await assertEqualAsync(await subject.participantPins, expected) + } + + // MARK: - setToken + + func test_setToken_shouldUpdateToken() async throws { + let expected = String.unique + + await subject.set(token: expected) + + await assertEqualAsync(await subject.token, expected) + } + + // MARK: - setIncomingVideoQualitySettings + + func test_setIncomingVideoQualitySettings_shouldUpdateIncomingVideoQualitySettings() async throws { + let expected = IncomingVideoQualitySettings.manual( + group: .custom(sessionIds: [.unique, .unique]), + targetSize: .init( + width: 11, + height: 10 + ) + ) + + await subject.set(incomingVideoQualitySettings: expected) + + await assertEqualAsync(await subject.incomingVideoQualitySettings, expected) + } + + // MARK: - setVideoFilter + + func test_setVideoFilter_shouldUpdateVideoFilter() async throws { + let sfuStack = MockSFUStack() + await subject.set(sfuAdapter: sfuStack.adapter) + try await subject.configurePeerConnections() + let expected = VideoFilter(id: .unique, name: .unique, filter: { _ in fatalError() }) + + await subject.set(videoFilter: expected) + + let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) + XCTAssertEqual( + mockPublisher.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.first?.id, + expected.id + ) + XCTAssertEqual( + mockPublisher.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.first?.name, + expected.name + ) + } + + // MARK: - refreshSession + + func test_refreshSession_shouldUpdateSessionID() async throws { + let currentSessionId = await subject.sessionID + + await subject.refreshSession() + + let newSessionId = await subject.sessionID + XCTAssertNotEqual(newSessionId, currentSessionId) + } + + // MARK: - configurePeerConnections + + func test_configurePeerConnections_withSFU_shouldSetupPeerConnections() async throws { + let sfuStack = MockSFUStack() + await subject.set(sfuAdapter: sfuStack.adapter) + let videoFilter = VideoFilter( + id: .unique, + name: .unique, + filter: { _ in fatalError() } + ) + await subject.set(videoFilter: videoFilter) + let ownCapabilities = Set([OwnCapability.blockUsers, .changeMaxDuration]) + await subject.set(ownCapabilities: ownCapabilities) + let callSettings = CallSettings(cameraPosition: .back) + await subject.set(callSettings: callSettings) + + try await subject.configurePeerConnections() + + let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) + let mockSubscriber = try await XCTAsyncUnwrap(await subject.subscriber as? MockRTCPeerConnectionCoordinator) + + XCTAssertEqual( + mockPublisher.recordedInputPayload( + (CallSettings, [OwnCapability]).self, + for: .setUp + )?.first?.0, + callSettings + ) + XCTAssertEqual( + mockPublisher.recordedInputPayload( + (CallSettings, [OwnCapability]).self, + for: .setUp + )?.first?.1.sorted(), + Array(ownCapabilities).sorted() + ) + XCTAssertEqual( + mockPublisher.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.first?.id, + videoFilter.id + ) + XCTAssertEqual( + mockPublisher.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.first?.name, + videoFilter.name + ) + + XCTAssertEqual( + mockSubscriber.recordedInputPayload( + (CallSettings, [OwnCapability]).self, + for: .setUp + )?.first?.0, + callSettings + ) + XCTAssertEqual( + mockSubscriber.recordedInputPayload( + (CallSettings, [OwnCapability]).self, + for: .setUp + )?.first?.1.sorted(), + Array(ownCapabilities).sorted() + ) + } + + func test_configurePeerConnections_withSFU_completesSetUp() async throws { + let sfuStack = MockSFUStack() + await subject.set(sfuAdapter: sfuStack.adapter) + let videoFilter = VideoFilter( + id: .unique, + name: .unique, + filter: { _ in fatalError() } + ) + await subject.set(videoFilter: videoFilter) + let ownCapabilities = Set([OwnCapability.blockUsers, .changeMaxDuration]) + await subject.set(ownCapabilities: ownCapabilities) + let callSettings = CallSettings(cameraPosition: .back) + await subject.set(callSettings: callSettings) + + try await subject.configurePeerConnections() + + await fulfillment { await self.subject.publisher != nil } + + let _publisher = await subject.publisher + let publisher = try XCTUnwrap(_publisher) + let _subscriber = await subject.subscriber + let subscriber = try XCTUnwrap(_subscriber) + + _ = await Task(timeout: 1) { + try await publisher.ensureSetUpHasBeenCompleted() + }.result + + _ = await Task(timeout: 1) { + try await subscriber.ensureSetUpHasBeenCompleted() + }.result + } + + func test_configurePeerConnections_withActiveSession_shouldBeginScreenSharing() async throws { + let sfuStack = MockSFUStack() + sfuStack.setConnectionState(to: .connected(healthCheckInfo: .init())) + await subject.set(sfuAdapter: sfuStack.adapter) + screenShareSessionProvider.activeSession = .init( + localTrack: await subject.peerConnectionFactory.mockVideoTrack(forScreenShare: true), + screenSharingType: .inApp, + capturer: MockStreamVideoCapturer() + ) + let ownCapabilities = Set([OwnCapability.blockUsers]) + await subject.set(ownCapabilities: ownCapabilities) + + try await subject.configurePeerConnections() + let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) + + XCTAssertEqual( + mockPublisher.recordedInputPayload( + (ScreensharingType, [OwnCapability]).self, + for: .beginScreenSharing + )?.first?.0, + .inApp + ) + XCTAssertEqual( + mockPublisher.recordedInputPayload( + (ScreensharingType, [OwnCapability]).self, + for: .beginScreenSharing + )?.first?.1, + [.blockUsers] + ) + } + + func test_configurePeerConnections_withoutActiveSession_shouldNotBeginScreenSharing() async throws { + let sfuStack = MockSFUStack() + sfuStack.setConnectionState(to: .connected(healthCheckInfo: .init())) + await subject.set(sfuAdapter: sfuStack.adapter) + let ownCapabilities = Set([OwnCapability.blockUsers]) + await subject.set(ownCapabilities: ownCapabilities) + + try await subject.configurePeerConnections() + let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) + + XCTAssertEqual(mockPublisher.timesCalled(.beginScreenSharing), 0) + } + + // MARK: - cleanUp + + func test_cleanUp_shouldResetProperties() async throws { + let sfuStack = MockSFUStack() + try await prepare(sfuStack: sfuStack) + let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) + let mockSubscriber = try await XCTAsyncUnwrap(await subject.subscriber as? MockRTCPeerConnectionCoordinator) + + await subject.cleanUp() + + XCTAssertEqual(mockPublisher.timesCalled(.close), 1) + XCTAssertEqual(mockSubscriber.timesCalled(.close), 1) + XCTAssertEqual(sfuStack.webSocket.timesCalled(.disconnectAsync), 1) + + await fulfillment { await self.subject.publisher == nil } + await assertNilAsync(await subject.publisher) + await assertNilAsync(await subject.subscriber) + await assertNilAsync(await subject.statsReporter) + await assertNilAsync(await subject.sfuAdapter) + await assertEqualAsync(await subject.token, "") + await assertEqualAsync(await subject.sessionID, "") + await assertEqualAsync(await subject.ownCapabilities, []) + await assertEqualAsync(await subject.participants, [:]) + await assertEqualAsync(await subject.participantsCount, 0) + await assertEqualAsync(await subject.anonymousCount, 0) + await assertEqualAsync(await subject.participantPins, []) + } + + // MARK: - cleanUpForReconnection + + func test_cleanUpForReconnection_shouldResetPropertiesForReconnection() async throws { + let sfuStack = MockSFUStack() + let ownCapabilities = Set([OwnCapability.blockUsers]) + let pins = [PinInfo(isLocal: true, pinnedAt: .init())] + let userId = String.unique + let participants = [userId: CallParticipant.dummy(id: userId)] + try await prepare( + sfuStack: sfuStack, + ownCapabilities: ownCapabilities, + participants: participants, + participantPins: pins + ) + let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) + let mockSubscriber = try await XCTAsyncUnwrap(await subject.subscriber as? MockRTCPeerConnectionCoordinator) + let sessionId = await subject.sessionID + await subject.didAddTrack( + .dummy( + kind: .video, + peerConnectionFactory: await subject.peerConnectionFactory + ), + type: .video, + for: userId + ) + await fulfillment { await self.subject.participants[userId]?.track != nil } + await subject.cleanUpForReconnection() + + XCTAssertEqual(mockPublisher.timesCalled(.close), 0) + XCTAssertEqual(mockSubscriber.timesCalled(.close), 0) + XCTAssertEqual(sfuStack.webSocket.timesCalled(.disconnectAsync), 0) + await assertNilAsync(await subject.publisher) + await assertNilAsync(await subject.subscriber) + await assertNilAsync(await subject.statsReporter) + await assertNilAsync(await subject.sfuAdapter) + await assertEqualAsync(await subject.token, "") + await assertEqualAsync(await subject.sessionID, sessionId) + await assertEqualAsync(await subject.ownCapabilities, ownCapabilities) + await assertEqualAsync(await subject.participants[userId]?.track, nil) + await assertEqualAsync(await subject.participantsCount, 12) + await assertEqualAsync(await subject.anonymousCount, 22) + await assertEqualAsync(await subject.participantPins, pins) + } + + // MARK: - didAddTrack + + func test_didAddTrack_videoOfExistingParticipant_shouldAddTrack() async throws { + let participant = CallParticipant.dummy() + let track = await subject + .peerConnectionFactory + .mockVideoTrack(forScreenShare: false) + await subject.enqueue { _ in [participant.sessionId: participant] } + + await subject.didAddTrack( + track, + type: .video, + for: participant.sessionId + ) + + await fulfillment { + await self + .subject + .participants[participant.sessionId]? + .track? + .trackId == track.trackId + } + } + + func test_didAddTrack_screenSharingOfExistingParticipant_shouldAddTrack() async throws { + let participant = CallParticipant.dummy() + let track = await subject + .peerConnectionFactory + .mockVideoTrack(forScreenShare: true) + await subject.enqueue { _ in [participant.sessionId: participant] } + + await subject.didAddTrack( + track, + type: .screenshare, + for: participant.sessionId + ) + + await fulfillment { + await self + .subject + .participants[participant.sessionId]? + .screenshareTrack? + .trackId == track.trackId + } + } + + // MARK: - didRemoveTrack + + func test_didRemoveTrack_videoOfExistingParticipant_shouldRemoveTrack() async throws { + let participant = CallParticipant.dummy() + let track = await subject + .peerConnectionFactory + .mockVideoTrack(forScreenShare: false) + await subject.enqueue { _ in [participant.sessionId: participant] } + await subject.didAddTrack(track, type: .video, for: participant.sessionId) + + await subject.didRemoveTrack( + for: participant.sessionId + ) + + await fulfillment { + await self + .subject + .participants[participant.sessionId]? + .track? + .trackId == nil + } + } + + func test_didRemoveTrack_screenSharingOfExistingParticipant_shouldRemoveTrack() async throws { + let participant = CallParticipant.dummy() + let track = await subject + .peerConnectionFactory + .mockVideoTrack(forScreenShare: true) + await subject.enqueue { _ in [participant.sessionId: participant] } + await subject.didAddTrack(track, type: .screenshare, for: participant.sessionId) + + await subject.didRemoveTrack( + for: participant.sessionId + ) + + await fulfillment { + await self + .subject + .participants[participant.sessionId]? + .screenshareTrack? + .trackId == nil + } + } + + // MARK: - trackFor + + func test_trackFor_withVideo_shouldReturnCorrectTrack() async throws { + let participant = CallParticipant.dummy() + let track = await subject + .peerConnectionFactory + .mockVideoTrack(forScreenShare: false) + await subject.enqueue { _ in [participant.sessionId: participant] } + await subject.didAddTrack(track, type: .video, for: participant.sessionId) + + let actual = await subject.track(for: participant.sessionId, of: .video) + + XCTAssertEqual(track.trackId, actual?.trackId) + } + + func test_trackFor_withScreenShare_shouldReturnCorrectTrack() async throws { + let participant = CallParticipant.dummy() + let track = await subject + .peerConnectionFactory + .mockVideoTrack(forScreenShare: true) + await subject.enqueue { _ in [participant.sessionId: participant] } + await subject.didAddTrack(track, type: .screenshare, for: participant.sessionId) + + let actual = await subject.track(for: participant.sessionId, of: .screenshare) + + XCTAssertEqual(track.trackId, actual?.trackId) + } + + // MARK: - didUpdateVideoOptions + + func test_didUpdateVideoOptions_shouldUpdateVideoOptions() async throws { + let sfuStack = MockSFUStack() + sfuStack.setConnectionState(to: .connected(healthCheckInfo: .init())) + await subject.set(sfuAdapter: sfuStack.adapter) + try await subject.configurePeerConnections() + let mockPublisher = try await XCTAsyncUnwrap(await subject.publisher as? MockRTCPeerConnectionCoordinator) + let mockSubscriber = try await XCTAsyncUnwrap(await subject.subscriber as? MockRTCPeerConnectionCoordinator) + let newVideoOptions = VideoOptions( + preferredCameraPosition: .back + ) + + await subject.set(videoOptions: newVideoOptions) + + XCTAssertEqual(mockPublisher.videoOptions.preferredCameraPosition, .back) + XCTAssertEqual(mockSubscriber.videoOptions.preferredCameraPosition, .back) + } + + // MARK: - didUpdateParticipants + + func test_didUpdateParticipants_shouldAssignTracksToParticipants() async throws { + let initialParticipants: [String: CallParticipant] = [ + "1": .dummy(id: "1"), + "2": .dummy(id: "2"), + "3": .dummy(id: "3") + ] + let participantTracks: [String: RTCMediaStreamTrack] = [ + "1": await subject.peerConnectionFactory.mockAudioTrack(), + "2": await subject.peerConnectionFactory.mockVideoTrack(forScreenShare: false), + "3": await subject.peerConnectionFactory.mockVideoTrack(forScreenShare: true) + ] + await subject.enqueue { _ in initialParticipants } + + await subject.didAddTrack(participantTracks["2"]!, type: .video, for: "2") + await subject.didAddTrack(participantTracks["3"]!, type: .screenshare, for: "3") + + await subject.enqueue { _ in initialParticipants } + + await fulfillment { + let participant2 = await self.subject.participants["2"] + let participant3 = await self.subject.participants["3"] + + return participant2?.track?.trackId == participantTracks["2"]?.trackId + && participant3?.screenshareTrack?.trackId == participantTracks["3"]?.trackId + } + } + + func test_didUpdateParticipants_withIncomingVideoQualitySettings_shouldAssignTracksToParticipantsCorrectly() async throws { + let initialParticipants: [String: CallParticipant] = [ + "1": .dummy(id: "1"), + "2": .dummy(id: "2"), + "3": .dummy(id: "3") + ] + let participantTracks: [String: RTCMediaStreamTrack] = [ + "1": await subject.peerConnectionFactory.mockAudioTrack(), + "2": await subject.peerConnectionFactory.mockVideoTrack(forScreenShare: false), + "3": await subject.peerConnectionFactory.mockVideoTrack(forScreenShare: true) + ] + await subject.set(incomingVideoQualitySettings: .disabled(group: .custom(sessionIds: ["2"]))) + await subject.enqueue { _ in initialParticipants } + + await subject.didAddTrack(participantTracks["2"]!, type: .video, for: "2") + await subject.didAddTrack(participantTracks["3"]!, type: .screenshare, for: "3") + + await subject.enqueue { _ in initialParticipants } + + await fulfillment { + let participant2 = await self.subject.participants["2"] + let participant3 = await self.subject.participants["3"] + + return participant2?.track == nil + && participant3?.screenshareTrack?.trackId == participantTracks["3"]?.trackId + } + } + + // MARK: - audioSessionDidUpdateCallSettings + + func test_audioSessionDidUpdateCallSettings_updatesCallSettingsAsExpected() async { + let updatedCallSettings = CallSettings( + audioOn: false, + videoOn: false, + speakerOn: true, + audioOutputOn: false, + cameraPosition: .back + ) + + subject.audioSessionAdapterDidUpdateCallSettings( + await subject.audioSession, + callSettings: updatedCallSettings + ) + + await fulfillment { [subject] in + await subject?.callSettings == updatedCallSettings + } + } + + // MARK: - Private helpers + + private func assertNilAsync( + _ expression: @autoclosure () async throws -> T?, + file: StaticString = #file, + line: UInt = #line + ) async rethrows { + let value = try await expression() + XCTAssertNil(value, file: file, line: line) + } + + private func assertEqualAsync( + _ expression: @autoclosure () async throws -> T, + _ expected: @autoclosure () async throws -> T, + file: StaticString = #file, + line: UInt = #line + ) async rethrows { + let value = try await expression() + let expectedValue = try await expected() + XCTAssertEqual(value, expectedValue, file: file, line: line) + } + + private func assertTrueAsync( + _ expression: @autoclosure () async throws -> Bool, + file: StaticString = #file, + line: UInt = #line + ) async rethrows { + let value = try await expression() + XCTAssertTrue(value, file: file, line: line) + } + + private func prepare( + sfuStack: MockSFUStack = .init(), + ownCapabilities: Set = [.changeMaxDuration], + participants: [String: CallParticipant] = [.unique: .dummy()], + participantPins: [PinInfo] = [.init(isLocal: true, pinnedAt: .init())] + ) async throws { + sfuStack.setConnectionState(to: .connected(healthCheckInfo: .init())) + let callSettings = CallSettings(cameraPosition: .back) + let videoFilter = VideoFilter( + id: .unique, + name: .unique, + filter: { _ in fatalError() } + ) + await fulfillment { await self.subject.sessionID.isEmpty == false } + await subject.set(sfuAdapter: sfuStack.adapter) + await subject.set(videoFilter: videoFilter) + await subject.set(ownCapabilities: ownCapabilities) + await subject.set(callSettings: callSettings) + await subject.set(token: .unique) + await subject.set(participantsCount: 12) + await subject.set(anonymousCount: 22) + await subject.set(participantPins: participantPins) + await subject.enqueue { _ in participants } + try await subject.configurePeerConnections() + await subject.set(statsReporter: WebRTCStatsReporter(sessionID: .unique)) + } +} + +extension OwnCapability: Comparable { + public static func < ( + lhs: OwnCapability, + rhs: OwnCapability + ) -> Bool { + lhs.rawValue <= rhs.rawValue + } +}