Files
element-x-ios/UnitTests/Sources/PresenceServiceTests.swift
T
2026-09-28 13:04:37 +03:00

239 lines
9.9 KiB
Swift

//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
@testable import ElementX
import Foundation
import Testing
import UIKit
@MainActor
struct PresenceServiceTests {
let clientProxy: ClientProxyMock
let userSettings: UserSettings
let notificationCenter: NotificationCenter
let sendsSubject = PassthroughSubject<(presence: ClientProxyPresence, sendImmediately: Bool), Never>()
init() {
clientProxy = ClientProxyMock(.init())
userSettings = UserSettings.volatile()
notificationCenter = NotificationCenter()
clientProxy.configurePresenceSendImmediatelyClosure = { [sendsSubject] presence, sendImmediately in
sendsSubject.send((presence, sendImmediately))
return .success(())
}
}
private func makeService(initialApplicationState: UIApplication.State) -> PresenceService {
PresenceService(clientProxy: clientProxy,
userSettings: userSettings,
notificationCenter: notificationCenter,
initialApplicationState: initialApplicationState)
}
@Test
func serviceSeededActiveWhileSharingReportsOnline() async throws {
userSettings.sharePresence = true
let deferred = deferFulfillment(sendsSubject) { $0.presence == .online }
let service = makeService(initialApplicationState: .active)
let send = try await deferred.fulfill()
#expect(send.presence == .online)
#expect(send.sendImmediately)
#expect(clientProxy.configurePresenceSendImmediatelyReceivedInvocations.filter { $0.presence == .online }.count == 1)
_ = service
}
@Test
func serviceSeededBackgroundedWhileSharingReportsUnavailable() async throws {
userSettings.sharePresence = true
let deferred = deferFulfillment(sendsSubject) { $0.presence == .unavailable }
let service = makeService(initialApplicationState: .background)
let send = try await deferred.fulfill()
#expect(send.presence == .unavailable)
#expect(send.sendImmediately)
_ = service
}
@Test
func serviceSeededActiveWhileNotSharingReportsOffline() async throws {
userSettings.sharePresence = false
let deferred = deferFulfillment(sendsSubject) { $0.presence == .offline }
let service = makeService(initialApplicationState: .active)
let send = try await deferred.fulfill()
#expect(send.presence == .offline)
#expect(send.sendImmediately)
_ = service
}
@Test
func becomingActiveNotificationWhileSharingReportsOnlineImmediately() async throws {
userSettings.sharePresence = true
let service = makeService(initialApplicationState: .inactive)
let deferred = deferFulfillment(sendsSubject) { $0.presence == .online }
notificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil)
let send = try await deferred.fulfill()
#expect(send.presence == .online)
#expect(send.sendImmediately)
_ = service
}
@Test
func resigningActiveNotificationWhileSharingReportsUnavailableImmediately() async throws {
userSettings.sharePresence = true
let initialDeferred = deferFulfillment(sendsSubject) { $0.presence == .online }
let service = makeService(initialApplicationState: .active)
_ = try await initialDeferred.fulfill()
let deferred = deferFulfillment(sendsSubject) { $0.presence == .unavailable }
notificationCenter.post(name: UIApplication.willResignActiveNotification, object: nil)
let send = try await deferred.fulfill()
#expect(send.presence == .unavailable)
#expect(send.sendImmediately)
_ = service
}
@Test
func togglingSharingWhileActiveDrivesOnlineAndOffline() async throws {
userSettings.sharePresence = false
let initialDeferred = deferFulfillment(sendsSubject) { $0.presence == .offline }
let service = makeService(initialApplicationState: .active)
_ = try await initialDeferred.fulfill()
let onlineDeferred = deferFulfillment(sendsSubject) { $0.presence == .online }
userSettings.sharePresence = true
let online = try await onlineDeferred.fulfill()
#expect(online.presence == .online)
#expect(online.sendImmediately)
let offlineDeferred = deferFulfillment(sendsSubject) { $0.presence == .offline }
userSettings.sharePresence = false
let offline = try await offlineDeferred.fulfill()
#expect(offline.presence == .offline)
#expect(offline.sendImmediately)
_ = service
}
@Test
func repeatingTheSameStateSendsOnlyOnce() async throws {
userSettings.sharePresence = true
let initialDeferred = deferFulfillment(sendsSubject) { $0.presence == .online }
let service = makeService(initialApplicationState: .active)
_ = try await initialDeferred.fulfill()
await Task.yield()
notificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil)
notificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil)
await Task.yield()
#expect(clientProxy.configurePresenceSendImmediatelyReceivedInvocations.map(\.presence) == [.online])
_ = service
}
@Test
func failedImmediateSendIsNotRetriedUntilTheDesiredPresenceChanges() async throws {
userSettings.sharePresence = true
clientProxy.configurePresenceSendImmediatelyClosure = { [sendsSubject] presence, sendImmediately in
sendsSubject.send((presence, sendImmediately))
return presence == .online ? .failure(.forbiddenAccess) : .success(())
}
let initialDeferred = deferFulfillment(sendsSubject) { $0.presence == .online }
let service = makeService(initialApplicationState: .active)
_ = try await initialDeferred.fulfill()
await Task.yield()
notificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil)
await Task.yield()
#expect(clientProxy.configurePresenceSendImmediatelyReceivedInvocations.map(\.presence) == [.online])
let transitionDeferred = deferFulfillment(sendsSubject) { $0.presence == .unavailable }
notificationCenter.post(name: UIApplication.willResignActiveNotification, object: nil)
_ = try await transitionDeferred.fulfill()
#expect(clientProxy.configurePresenceSendImmediatelyReceivedInvocations.map(\.presence) == [.online, .unavailable])
_ = service
}
@Test
func newerTransitionWinsWhenAnEarlierSendIsInFlight() async throws {
userSettings.sharePresence = true
let (onlineStartedStream, onlineStartedContinuation) = AsyncStream<Void>.makeStream()
let (resumeOnlineSendStream, resumeOnlineSendContinuation) = AsyncStream<Void>.makeStream()
clientProxy.configurePresenceSendImmediatelyClosure = { [sendsSubject] presence, sendImmediately in
if presence == .online {
onlineStartedContinuation.yield()
var iterator = resumeOnlineSendStream.makeAsyncIterator()
_ = await iterator.next()
}
sendsSubject.send((presence, sendImmediately))
return .success(())
}
let onlineStartedDeferred = deferFulfillment(onlineStartedStream) { _ in true }
let service = makeService(initialApplicationState: .active)
try await onlineStartedDeferred.fulfill()
let unavailableDeferred = deferFulfillment(sendsSubject) { $0.presence == .unavailable }
notificationCenter.post(name: UIApplication.willResignActiveNotification, object: nil)
resumeOnlineSendContinuation.yield()
resumeOnlineSendContinuation.finish()
try await unavailableDeferred.fulfill()
let applied = clientProxy.configurePresenceSendImmediatelyReceivedInvocations.map(\.presence)
#expect(applied == [.online, .unavailable], "Expected in-order latest-wins drain, got \(applied)")
_ = service
}
@Test
func deinitCancelsPendingSend() async throws {
userSettings.sharePresence = true
let (onlineStartedStream, onlineStartedContinuation) = AsyncStream.makeStream(of: Void.self)
let (onlineFinishedStream, onlineFinishedContinuation) = AsyncStream.makeStream(of: Void.self)
clientProxy.configurePresenceSendImmediatelyClosure = { presence, _ in
if presence == .online {
onlineStartedContinuation.yield()
try? await Task.sleep(for: .seconds(3600)) // Interrupted by the service's deinit cancelling the send task.
onlineFinishedContinuation.yield()
}
return .success(())
}
let onlineStartedDeferred = deferFulfillment(onlineStartedStream) { _ in true }
var service: PresenceService? = makeService(initialApplicationState: .active)
#expect(service != nil)
try await onlineStartedDeferred.fulfill()
// Queue an unavailable send behind the in-flight online one.
notificationCenter.post(name: UIApplication.willResignActiveNotification, object: nil)
let onlineFinishedDeferred = deferFulfillment(onlineFinishedStream) { _ in true }
service = nil
try await onlineFinishedDeferred.fulfill()
let applied = clientProxy.configurePresenceSendImmediatelyReceivedInvocations.map(\.presence)
#expect(applied == [.online], "Expected deinit to cancel the queued unavailable send, got \(applied)")
}
}