mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
ab39d8de06
The sync update action is sent from the room list service state listener, which fires before the SDK has applied the new unread counts, leaving the app badge one sync behind. Drive both the badge and the clearing of delivered notifications for fully read rooms from the room list publisher instead, moving the observer from the ChatsTabFlowCoordinator into the UserSessionFlowCoordinator. The NotificationManager now receives the badge count from the caller so the same value can be reused elsewhere.
304 lines
14 KiB
Swift
304 lines
14 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import Combine
|
|
@testable import ElementX
|
|
import MatrixRustSDKMocks
|
|
import NotificationCenter
|
|
import Testing
|
|
|
|
@MainActor
|
|
final class NotificationManagerTests {
|
|
var notificationManager: NotificationManager!
|
|
private let clientProxy = ClientProxyMock(.init(userID: "@test:user.net"))
|
|
private lazy var mockUserSession = UserSessionMock(.init(clientProxy: clientProxy))
|
|
private var notificationCenter: UserNotificationCenterMock!
|
|
private var authorizationStatusWasGranted = false
|
|
private var shouldDisplayInAppNotificationReturnValue = false
|
|
private var handleInlineReplyDelegateCalled = false
|
|
private var notificationTappedDelegateCalled = false
|
|
private var registerForRemoteNotificationsDelegateCalled: (() -> Void)?
|
|
private let appSettings: AppSettings
|
|
|
|
init() {
|
|
appSettings = AppSettings.volatile()
|
|
notificationCenter = UserNotificationCenterMock()
|
|
notificationCenter.requestAuthorizationOptionsReturnValue = true
|
|
notificationCenter.authorizationStatusReturnValue = .authorized
|
|
notificationCenter.notificationSettingsClosure = { await UNUserNotificationCenter.current().notificationSettings() }
|
|
|
|
notificationManager = NotificationManager(notificationCenter: notificationCenter, appSettings: appSettings)
|
|
notificationManager.start()
|
|
notificationManager.setUserSession(mockUserSession)
|
|
}
|
|
|
|
isolated deinit {
|
|
notificationCenter = nil
|
|
notificationManager = nil
|
|
}
|
|
|
|
@Test
|
|
func whenRegistered_pusherIsCalled() async {
|
|
_ = await notificationManager.register(with: Data())
|
|
|
|
#expect(clientProxy.setPusherWithCalled)
|
|
}
|
|
|
|
@Test
|
|
func whenRegisteredSuccess_completionSuccessIsCalled() async {
|
|
let success = await notificationManager.register(with: Data())
|
|
#expect(success)
|
|
}
|
|
|
|
@Test
|
|
func whenRegisteredAndPusherThrowsError_completionFalseIsCalled() async {
|
|
enum TestError: Error {
|
|
case someError
|
|
}
|
|
|
|
clientProxy.setPusherWithThrowableError = TestError.someError
|
|
let success = await notificationManager.register(with: Data())
|
|
#expect(!success)
|
|
}
|
|
|
|
@Test
|
|
func whenRegistered_pusherIsCalledWithCorrectValues() async throws {
|
|
let pushkeyData = Data("1234".utf8)
|
|
_ = await notificationManager.register(with: pushkeyData)
|
|
|
|
guard let configuration = clientProxy.setPusherWithReceivedInvocations.first else {
|
|
Issue.record("Invalid pusher configuration sent")
|
|
return
|
|
}
|
|
|
|
#expect(configuration.identifiers.pushkey == pushkeyData.base64EncodedString())
|
|
#expect(configuration.identifiers.appId == appSettings.pusherAppID)
|
|
#expect(configuration.appDisplayName == "\(InfoPlistReader.main.bundleDisplayName) (iOS)")
|
|
#expect(configuration.deviceDisplayName == UIDevice.current.name)
|
|
#expect(configuration.profileTag != nil)
|
|
#expect(configuration.lang == Bundle.app.preferredLocalizations.first)
|
|
guard case let .http(data) = configuration.kind else {
|
|
Issue.record("Http kind expected")
|
|
return
|
|
}
|
|
#expect(data.url == appSettings.pushGatewayNotifyEndpoint.absoluteString)
|
|
#expect(data.format == .eventIdOnly)
|
|
let defaultPayload = APNSPayload(aps: APSInfo(mutableContent: 1,
|
|
alert: APSAlert(locKey: "Notification",
|
|
locArgs: [])),
|
|
pusherNotificationClientIdentifier: nil)
|
|
#expect(try data.defaultPayload == (defaultPayload.toJsonString()))
|
|
}
|
|
|
|
@Test
|
|
func whenRegisteredAndPusherTagNotSetInSettings_tagGeneratedAndSavedInSettings() async {
|
|
appSettings.pusherProfileTag = nil
|
|
_ = await notificationManager.register(with: Data())
|
|
#expect(appSettings.pusherProfileTag != nil)
|
|
}
|
|
|
|
@Test
|
|
func whenRegisteredAndPusherTagIsSetInSettings_tagNotGenerated() async {
|
|
appSettings.pusherProfileTag = "12345"
|
|
_ = await notificationManager.register(with: Data())
|
|
#expect(appSettings.pusherProfileTag == "12345")
|
|
}
|
|
|
|
@Test
|
|
func whenShowLocalNotification_notificationRequestGetsAdded() async throws {
|
|
await notificationManager.showLocalNotification(with: "Title", subtitle: "Subtitle")
|
|
let request = try #require(notificationCenter.addReceivedRequest)
|
|
#expect(request.content.title == "Title")
|
|
#expect(request.content.subtitle == "Subtitle")
|
|
}
|
|
|
|
@Test
|
|
func whenStart_notificationCategoriesAreSet() {
|
|
let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply,
|
|
title: L10n.actionQuickReply,
|
|
options: [])
|
|
let messageCategory = UNNotificationCategory(identifier: NotificationConstants.Category.message,
|
|
actions: [replyAction],
|
|
intentIdentifiers: [],
|
|
options: [])
|
|
|
|
let inviteCategory = UNNotificationCategory(identifier: NotificationConstants.Category.invite,
|
|
actions: [],
|
|
intentIdentifiers: [],
|
|
options: [])
|
|
#expect(notificationCenter.setNotificationCategoriesReceivedCategories == [messageCategory, inviteCategory])
|
|
}
|
|
|
|
@Test
|
|
func whenStart_delegateIsSet() throws {
|
|
let delegate = try #require(notificationCenter.delegate)
|
|
#expect(delegate.isEqual(notificationManager))
|
|
}
|
|
|
|
@Test
|
|
func whenStart_requestAuthorizationCalledWithCorrectParams() async {
|
|
await waitForConfirmation("requestAuthorization should be called", timeout: .seconds(10)) { confirm in
|
|
notificationCenter.requestAuthorizationOptionsClosure = { _ in
|
|
confirm()
|
|
return true
|
|
}
|
|
notificationManager.requestAuthorization()
|
|
}
|
|
#expect(notificationCenter.requestAuthorizationOptionsReceivedOptions == [.alert, .sound, .badge])
|
|
}
|
|
|
|
@Test
|
|
func whenStartAndAuthorizationGranted_delegateCalled() async {
|
|
authorizationStatusWasGranted = false
|
|
notificationManager.delegate = self
|
|
await waitForConfirmation("registerForRemoteNotifications delegate function should be called", timeout: .seconds(10)) { confirm in
|
|
registerForRemoteNotificationsDelegateCalled = {
|
|
confirm()
|
|
}
|
|
notificationManager.requestAuthorization()
|
|
}
|
|
#expect(authorizationStatusWasGranted)
|
|
}
|
|
|
|
@Test
|
|
func whenStartAndAuthorizedAndNotificationDisabled_registerForRemoteNotificationsNotCalled() async throws {
|
|
appSettings.enableNotifications = false
|
|
notificationCenter.authorizationStatusReturnValue = .authorized
|
|
notificationManager.delegate = self
|
|
|
|
notificationManager.setUserSession(UserSessionMock(.init()))
|
|
try await Task.sleep(for: .seconds(1))
|
|
|
|
#expect(!authorizationStatusWasGranted)
|
|
}
|
|
|
|
@Test
|
|
func whenStartAndAuthorized_registerForRemoteNotificationsCalled() async {
|
|
appSettings.enableNotifications = true
|
|
notificationCenter.authorizationStatusReturnValue = .authorized
|
|
notificationManager.delegate = self
|
|
|
|
await waitForConfirmation("registerForRemoteNotifications delegate function should be called", timeout: .seconds(10)) { confirm in
|
|
registerForRemoteNotificationsDelegateCalled = {
|
|
confirm()
|
|
}
|
|
notificationManager.setUserSession(UserSessionMock(.init()))
|
|
}
|
|
|
|
#expect(authorizationStatusWasGranted)
|
|
}
|
|
|
|
@Test
|
|
func removeDeliveredNotificationsForFullyReadRooms() async throws {
|
|
let room = RoomSummary(room: RoomSDKMock(), id: "!room:matrix.org", settingsMode: .allMessages,
|
|
hasUnreadMessages: false, hasUnreadMentions: false, hasUnreadNotifications: false)
|
|
let lastMessageDate = try #require(room.lastMessageDate)
|
|
|
|
// Delivered after the last message was sent, which is always the case for the last message's notification.
|
|
let readNotification = try UNNotification.with(userInfo: [NotificationConstants.UserInfoKey.roomIdentifier: room.id,
|
|
NotificationConstants.UserInfoKey.eventDate: lastMessageDate.addingTimeInterval(0.5)],
|
|
identifier: "read",
|
|
date: lastMessageDate.addingTimeInterval(5))
|
|
let newerNotification = try UNNotification.with(userInfo: [NotificationConstants.UserInfoKey.roomIdentifier: room.id,
|
|
NotificationConstants.UserInfoKey.eventDate: lastMessageDate.addingTimeInterval(1)],
|
|
identifier: "newer",
|
|
date: lastMessageDate.addingTimeInterval(5))
|
|
notificationCenter.deliveredNotificationsReturnValue = [readNotification, newerNotification]
|
|
|
|
await notificationManager.removeDeliveredNotificationsForFullyReadRooms([room])
|
|
|
|
#expect(notificationCenter.removeDeliveredNotificationsWithIdentifiersReceivedIdentifiers == ["read"])
|
|
}
|
|
|
|
@Test
|
|
func whenWillPresentNotificationsDelegateNotSet_CorrectPresentationOptionsReturned() async throws {
|
|
let archiver = MockCoder(requiringSecureCoding: false)
|
|
let notification = try #require(UNNotification(coder: archiver))
|
|
let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification)
|
|
#expect(options == [.badge, .sound, .list, .banner])
|
|
}
|
|
|
|
@Test
|
|
func whenWillPresentNotificationsDelegateSetAndNotificationsShoudNotBeDisplayed_CorrectPresentationOptionsReturned() async throws {
|
|
shouldDisplayInAppNotificationReturnValue = false
|
|
notificationManager.delegate = self
|
|
|
|
let notification = try UNNotification.with(userInfo: [AnyHashable: Any]())
|
|
let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification)
|
|
#expect(options == [])
|
|
}
|
|
|
|
@Test
|
|
func whenWillPresentNotificationsDelegateSetAndNotificationsShoudBeDisplayed_CorrectPresentationOptionsReturned() async throws {
|
|
shouldDisplayInAppNotificationReturnValue = true
|
|
notificationManager.delegate = self
|
|
|
|
let notification = try UNNotification.with(userInfo: [AnyHashable: Any]())
|
|
let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification)
|
|
#expect(options == [.badge, .sound, .list, .banner])
|
|
}
|
|
|
|
@Test
|
|
func whenNotificationCenterReceivedResponseInLineReply_delegateIsCalled() async throws {
|
|
handleInlineReplyDelegateCalled = false
|
|
notificationManager.delegate = self
|
|
let response = try UNTextInputNotificationResponse.with(userInfo: [AnyHashable: Any](), actionIdentifier: NotificationConstants.Action.inlineReply)
|
|
await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: response)
|
|
#expect(handleInlineReplyDelegateCalled)
|
|
}
|
|
|
|
@Test
|
|
func whenNotificationCenterReceivedResponseWithActionIdentifier_delegateIsCalled() async throws {
|
|
notificationTappedDelegateCalled = false
|
|
notificationManager.delegate = self
|
|
let response = try UNTextInputNotificationResponse.with(userInfo: [AnyHashable: Any](), actionIdentifier: UNNotificationDefaultActionIdentifier)
|
|
await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: response)
|
|
#expect(notificationTappedDelegateCalled)
|
|
}
|
|
|
|
@Test
|
|
func updatingAppBadgeCount() async {
|
|
await notificationManager.updateAppBadgeCount(7)
|
|
|
|
#expect(notificationCenter.setBadgeCountReceivedNewBadgeCount == 7)
|
|
#expect(appSettings.lastKnownBadgeCount == 7)
|
|
}
|
|
|
|
@Test
|
|
func updatingAppBadgeCountWithoutASessionDoesNothing() async {
|
|
notificationManager.setUserSession(nil)
|
|
|
|
await notificationManager.updateAppBadgeCount(7)
|
|
|
|
#expect(!notificationCenter.setBadgeCountCalled)
|
|
}
|
|
}
|
|
|
|
extension NotificationManagerTests: @MainActor NotificationManagerDelegate {
|
|
func registerForRemoteNotifications() {
|
|
authorizationStatusWasGranted = true
|
|
registerForRemoteNotificationsDelegateCalled?()
|
|
}
|
|
|
|
func unregisterForRemoteNotifications() {
|
|
authorizationStatusWasGranted = false
|
|
}
|
|
|
|
func shouldDisplayInAppNotification(content: UNNotificationContent) -> Bool {
|
|
shouldDisplayInAppNotificationReturnValue
|
|
}
|
|
|
|
func notificationTapped(content: UNNotificationContent) async {
|
|
notificationTappedDelegateCalled = true
|
|
}
|
|
|
|
func handleInlineReply(_ service: ElementX.NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async {
|
|
handleInlineReplyDelegateCalled = true
|
|
}
|
|
}
|