Skip to content

Commit aabb5ea

Browse files
committed
Male CallService testable + tests
1 parent 93257c6 commit aabb5ea

File tree

5 files changed

+106
-67
lines changed

5 files changed

+106
-67
lines changed

ElementX/Sources/Services/ElementCall/ElementCallService.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import MatrixRustSDK
1414
import PushKit
1515
import UIKit
1616

17+
// Keep this class testable
18+
struct Time {
19+
var clock: any Clock<Duration>
20+
var nowDate: () -> Date
21+
}
22+
1723
class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDelegate, CXProviderDelegate {
1824
private struct CallID: Equatable {
1925
let callKitID: UUID
@@ -24,6 +30,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
2430
private let pushRegistry: PKPushRegistry
2531
private let callController = CXCallController()
2632
private let callProvider: CXProviderProtocol
33+
private let timeClock: Time
2734

2835
private weak var clientProxy: ClientProxyProtocol? {
2936
didSet {
@@ -59,10 +66,12 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
5966

6067
private var declineListenerHandle: TaskHandle?
6168

62-
init(callProvider: CXProviderProtocol? = nil) {
69+
init(callProvider: CXProviderProtocol? = nil, timeClock: Time? = nil) {
6370
pushRegistry = PKPushRegistry(queue: nil)
6471

65-
if let callProvider = callProvider {
72+
self.timeClock = timeClock ?? Time(clock: ContinuousClock(), nowDate: Date.init)
73+
74+
if let callProvider {
6675
self.callProvider = callProvider
6776
} else {
6877
let configuration = CXProviderConfiguration()
@@ -168,11 +177,11 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
168177
let callID = CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: rtcNotificationID)
169178
incomingCallID = callID
170179

171-
guard let expirationTimestamp = payload.dictionaryPayload[ElementCallServiceNotificationKey.expirationTimestampMillis.rawValue] as? UInt64 else {
180+
guard let expirationTimestamp = (payload.dictionaryPayload[ElementCallServiceNotificationKey.expirationTimestampMillis.rawValue] as? NSNumber)?.uint64Value else {
172181
MXLog.error("Something went wrong, missing expiration timestamp for incoming voip call: \(payload)")
173182
return
174183
}
175-
let nowTimestampMillis = UInt64(Date().timeIntervalSince1970 * 1000)
184+
let nowTimestampMillis = UInt64(timeClock.nowDate().timeIntervalSince1970 * 1000)
176185

177186
guard nowTimestampMillis < expirationTimestamp else {
178187
MXLog.warning("Call expired for room \(roomID), ignoring incoming push")
@@ -200,7 +209,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
200209
}
201210

202211
endUnansweredCallTask = Task { [weak self] in
203-
try? await Task.sleep(for: .milliseconds(ringDurationMillis))
212+
try? await self?.timeClock.clock.sleep(for: .milliseconds(ringDurationMillis))
204213

205214
guard let self, !Task.isCancelled else {
206215
return

ElementX/Sources/Services/Room/JoinedRoomProxy.swift

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -837,57 +837,3 @@ private final class RoomKnockRequestsListener: KnockRequestsListener {
837837
onUpdateClosure(joinRequests)
838838
}
839839
}
840-
841-
final class RoomCallDeclineListener: CallDeclineListener {
842-
private let onUpdateClosure: (RtcDeclinedEvent) -> Void
843-
private let notificationId: String
844-
845-
init(notificationId: String, onUpdateClosure: @escaping (RtcDeclinedEvent) -> Void) {
846-
self.notificationId = notificationId
847-
self.onUpdateClosure = onUpdateClosure
848-
}
849-
850-
func call(declinerUserId: String) {
851-
onUpdateClosure(.init(sender: declinerUserId, notificationEventId: notificationId))
852-
}
853-
}
854-
855-
// Helper to transform callback to publisher while correctly retaining the task handle and listener.
856-
struct DeclineCallbackPublisher: Publisher {
857-
typealias Output = RtcDeclinedEvent
858-
typealias Failure = Never
859-
860-
let room: JoinedRoomProxy
861-
let eventId: String
862-
863-
func receive<S>(subscriber: S) where S: Subscriber, Never == S.Failure, RtcDeclinedEvent == S.Input {
864-
let subscription = Inner(subscriber: subscriber, room: room, eventId: eventId)
865-
subscriber.receive(subscription: subscription)
866-
}
867-
868-
private final class Inner<S: Subscriber>: Subscription
869-
where S.Input == RtcDeclinedEvent, S.Failure == Never {
870-
private var subscriber: S?
871-
private var handle: TaskHandle?
872-
private var listener: RoomCallDeclineListener?
873-
874-
init(subscriber: S, room: JoinedRoomProxy, eventId: String) {
875-
self.subscriber = subscriber
876-
listener = RoomCallDeclineListener(notificationId: eventId) { [weak self] ev in
877-
_ = self?.subscriber?.receive(ev)
878-
}
879-
handle = try? room.subscribeToCallDeclineEvents(rtcNotificationEventId: eventId, listener: listener!).get()
880-
}
881-
882-
func request(_ demand: Subscribers.Demand) {
883-
// nop
884-
}
885-
886-
func cancel() {
887-
handle?.cancel()
888-
handle = nil
889-
listener = nil
890-
subscriber = nil
891-
}
892-
}
893-
}

ElementX/SupportingFiles/target.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ targets:
227227
- package: Algorithms
228228
- package: AnalyticsEvents
229229
- package: Collections
230+
- package: Clocks
230231
- package: DeviceKit
231232
- package: DTCoreText
232233
- package: EmbeddedElementCall

UnitTests/Sources/ElementCallServiceTests.swift

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Please see LICENSE files in the repository root for full details.
66
//
77

8+
import Clocks
89
import PushKit
910
import XCTest
1011

@@ -13,39 +14,118 @@ import XCTest
1314
@MainActor
1415
class ElementCallServiceTests: XCTestCase {
1516
var callProvider: CXProviderMock!
17+
var currentDate: Date!
18+
var testClock: TestClock<Duration>!
1619
let pushRegistry = PKPushRegistry(queue: nil)
1720

1821
var service: ElementCallService!
1922

2023
func testIncomingCall() async {
2124
setupService()
25+
2226
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
2327

2428
let expectation = XCTestExpectation(description: "Call accepted")
2529

26-
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: PKPushPayloadMock(), for: .voIP) {
30+
let pkPushPayloadMock = PKPushPayloadMock().addSeconds(currentDate, lifetime: 30)
31+
32+
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: pkPushPayloadMock, for: .voIP) {
2733
expectation.fulfill()
2834
}
2935

3036
await fulfillment(of: [expectation], timeout: 1)
3137
XCTAssertTrue(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
3238
}
3339

40+
func testCallIsTimingOut() async {
41+
setupService()
42+
43+
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
44+
45+
let expectation = XCTestExpectation(description: "Call accepted")
46+
47+
let pushPayload = PKPushPayloadMock().addSeconds(currentDate, lifetime: 20)
48+
49+
service.pushRegistry(pushRegistry,
50+
didReceiveIncomingPushWith: pushPayload,
51+
for: .voIP) {
52+
expectation.fulfill()
53+
}
54+
await fulfillment(of: [expectation], timeout: 1)
55+
56+
// advance past the timeout
57+
await testClock.advance(by: .seconds(30))
58+
59+
// Call should have ended with unanswered
60+
XCTAssertTrue(callProvider.reportCallWithEndedAtReasonCalled)
61+
XCTAssertEqual(callProvider.reportCallWithEndedAtReasonReceivedArguments?.reason, .unanswered)
62+
}
63+
64+
func testExpiredRingLifetimeIsIgnored() async {
65+
setupService()
66+
67+
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
68+
69+
let pushPayload = PKPushPayloadMock().addSeconds(currentDate, lifetime: 20)
70+
71+
currentDate = currentDate.addingTimeInterval(60)
72+
73+
service.pushRegistry(pushRegistry,
74+
didReceiveIncomingPushWith: pushPayload,
75+
for: .voIP) { }
76+
sleep(20)
77+
78+
XCTAssertTrue(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
79+
}
80+
81+
func testLifetimeIsCapped() async {
82+
setupService()
83+
84+
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
85+
86+
let pushPayload = PKPushPayloadMock().addSeconds(currentDate, lifetime: 300)
87+
88+
service.pushRegistry(pushRegistry,
89+
didReceiveIncomingPushWith: pushPayload,
90+
for: .voIP) { }
91+
92+
// advance pass the max timeout but below the 300
93+
await testClock.advance(by: .seconds(100))
94+
95+
// Call should have ended with unanswered
96+
XCTAssertTrue(callProvider.reportCallWithEndedAtReasonCalled)
97+
XCTAssertEqual(callProvider.reportCallWithEndedAtReasonReceivedArguments?.reason, .unanswered)
98+
}
99+
34100
// MARK: - Helpers
35101

36102
private func setupService() {
37103
callProvider = CXProviderMock(.init())
38-
service = ElementCallService(callProvider: callProvider)
104+
currentDate = Date()
105+
testClock = TestClock()
106+
let dateProvider: () -> Date = {
107+
self.currentDate
108+
}
109+
service = ElementCallService(callProvider: callProvider, timeClock: Time(clock: testClock, nowDate: dateProvider))
39110
}
40111
}
41112

42113
private class PKPushPayloadMock: PKPushPayload {
114+
var dict: [AnyHashable: Any] = [:]
115+
116+
override init() {
117+
dict[ElementCallServiceNotificationKey.roomID.rawValue] = "!room:example.com"
118+
dict[ElementCallServiceNotificationKey.roomDisplayName.rawValue] = "welcome"
119+
dict[ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue] = "$000"
120+
dict[ElementCallServiceNotificationKey.expirationTimestampMillis.rawValue] = 10
121+
}
122+
43123
override var dictionaryPayload: [AnyHashable: Any] {
44-
[
45-
ElementCallServiceNotificationKey.roomID.rawValue: "1",
46-
ElementCallServiceNotificationKey.roomDisplayName.rawValue: "Test",
47-
ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue: "a",
48-
ElementCallServiceNotificationKey.expirationTimestampMillis.rawValue: UInt64((Date().timeIntervalSince1970 + 5) * 1000)
49-
]
124+
dict
125+
}
126+
127+
func addSeconds(_ from: Date, lifetime: Int) -> Self {
128+
dict[ElementCallServiceNotificationKey.expirationTimestampMillis.rawValue] = UInt64(from.timeIntervalSince1970 * 1000) + UInt64(lifetime)
129+
return self
50130
}
51131
}

project.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ packages:
102102
AsyncAlgorithms:
103103
url: https://github.com/apple/swift-async-algorithms
104104
minorVersion: 1.0.0
105+
Clocks:
106+
url: https://github.com/pointfreeco/swift-clocks
107+
from: 1.0.6
105108
Collections:
106109
url: https://github.com/apple/swift-collections
107110
minorVersion: 1.2.0

0 commit comments

Comments
 (0)