Skip to content

Commit

Permalink
Add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Feb 19, 2024
1 parent faa3385 commit 3a77cf9
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,9 @@ public struct RegisterRemoteNotificationsAction {
case concurrentAccess
}

private let application: _Application
private weak var spezi: Spezi?

init(_ spezi: Spezi) {
#if os(watchOS)
self.application = _Application.shared()
#else
self.application = _Application.shared
#endif
self.spezi = spezi
}

Expand All @@ -81,6 +75,12 @@ public struct RegisterRemoteNotificationsAction {
preconditionFailure("RegisterRemoteNotificationsAction was used in a scope where Spezi was not available anymore!")
}

#if os(watchOS)
let application = _Application.shared()
#else
let application = _Application.shared
#endif

let registration = spezi.storage[RemoteNotificationContinuation.self]
if registration.continuation != nil {
throw ActionError.concurrentAccess
Expand Down Expand Up @@ -132,6 +132,7 @@ extension RegisterRemoteNotificationsAction {
// This can be handled through the `NotificationHandler` protocol.
return
}
registration.continuation = nil
continuation.resume(returning: deviceToken)
}

Expand All @@ -142,6 +143,7 @@ extension RegisterRemoteNotificationsAction {
spezi.logger.warning("Received a call to \(#function) while we were not waiting for a notifications registration request.")
return
}
registration.continuation = nil
continuation.resume(throwing: error)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,17 @@ import SwiftUI
/// }
/// ```
public struct UnregisterRemoteNotificationsAction {
private let application: _Application


init() {
#if os(watchOS)
self.application = _Application.shared()
#else
self.application = _Application.shared
#endif
}
init() {}


/// Unregisters for all remote notifications received through Apple Push Notification service.
public func callAsFunction() {
#if os(watchOS)
let application = _Application.shared()
#else
let application = _Application.shared
#endif

application.unregisterForRemoteNotifications()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ final class LifecycleTests: XCTestCase {
)

XCTAssertTrue(module.launchOptions.keys.allSatisfy { launchOptions[$0] != nil })
#elseif os(watchOS)
testApplicationDelegate.applicationDidFinishLaunching()
#endif

#if os(iOS) || os(visionOS) || os(tvOS)
testApplicationDelegate.applicationWillTerminate(UIApplication.shared)
wait(for: [expectationApplicationWillTerminate])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

@testable import Spezi
import SwiftUI
import UserNotifications
import XCTest


private final class TestNotificationHandler: Module, NotificationHandler, NotificationTokenHandler {
@Application(\.registerRemoteNotifications)
var registerRemoteNotifications
@Application(\.unregisterRemoteNotifications)
var unregisterRemoteNotifications

private let actionExpectation: XCTestExpectation
private let incomingNotificationExpectation: XCTestExpectation
private let remoteNotificationExpectation: XCTestExpectation
#if !os(macOS)
private var backgroundFetchResult: BackgroundFetchResult = .noData
#endif

var lastDeviceToken: Data?

init(
actionExpectation: XCTestExpectation = .init(),
incomingNotificationExpectation: XCTestExpectation = .init(),
remoteNotificationExpectation: XCTestExpectation = .init()
) {
self.actionExpectation = actionExpectation
self.incomingNotificationExpectation = incomingNotificationExpectation
self.remoteNotificationExpectation = remoteNotificationExpectation
}

#if !os(macOS)
func setFetchResult(_ fetchResult: BackgroundFetchResult) {
self.backgroundFetchResult = fetchResult
}
#endif


func receiveUpdatedDeviceToken(_ deviceToken: Data) {
lastDeviceToken = deviceToken
}

func handleNotificationAction(_ response: UNNotificationResponse) async {
actionExpectation.fulfill()
}

func receiveIncomingNotification(_ notification: UNNotification) async -> UNNotificationPresentationOptions? {
incomingNotificationExpectation.fulfill()
return [.badge, .banner]
}

#if !os(macOS)
func receiveRemoteNotification(_ remoteNotification: [AnyHashable: Any]) async -> BackgroundFetchResult {
remoteNotificationExpectation.fulfill()
return backgroundFetchResult
}
#else
func receiveRemoteNotification(_ remoteNotification: [AnyHashable: Any]) {
remoteNotificationExpectation.fulfill()
}
#endif
}


private final class EmptyNotificationHandler: Module, NotificationHandler {}


private class TestNotificationApplicationDelegate: SpeziAppDelegate {
private let injectedModule: TestNotificationHandler

override var configuration: Configuration {
Configuration {
injectedModule
EmptyNotificationHandler() // ensure default implementations don't interfere with the tests
}
}

init(_ injectedModule: TestNotificationHandler) {
self.injectedModule = injectedModule
}
}


final class NotificationsTests: XCTestCase {
@MainActor
func testRegisterNotificationsSuccessful() async throws {
let module = TestNotificationHandler()
let delegate = TestNotificationApplicationDelegate(module)
_ = delegate.spezi // init spezi

let action = module.registerRemoteNotifications

let expectation = XCTestExpectation(description: "RegisterRemoteNotifications")
var caught: Error?

Task { // this task also runs on main actor
do {
try await action()
} catch {
caught = error
}
expectation.fulfill()
}

try await Task.sleep(for: .milliseconds(500)) // allow dispatch of Task above

let data = try XCTUnwrap("Hello World".data(using: .utf8))

#if os(iOS) || os(visionOS) || os(tvOS)
delegate.application(UIApplication.shared, didRegisterForRemoteNotificationsWithDeviceToken: data)
#elseif os(watchOS)
delegate.application(WKApplication.shared(), didRegisterForRemoteNotificationsWithDeviceToken: data)
#elseif os(macOS)
delegate.application(NSApplication.shared, didRegisterForRemoteNotificationsWithDeviceToken: data)
#endif

try await Task.sleep(for: .milliseconds(500)) // allow dispatch of Task above

wait(for: [expectation])
XCTAssertNil(caught)
XCTAssertEqual(module.lastDeviceToken, data)
}

@MainActor
func testRegisterNotificationsErroneous() async throws {
enum TestError: Error, Equatable {
case testError
}

let module = TestNotificationHandler()
let delegate = TestNotificationApplicationDelegate(module)
_ = delegate.spezi // init spezi

let action = module.registerRemoteNotifications

let expectation = XCTestExpectation(description: "RegisterRemoteNotifications")
var caught: Error?

Task { // this task also runs on main actor
do {
try await action()
} catch {
caught = error
}
expectation.fulfill()
}

try await Task.sleep(for: .milliseconds(500)) // allow dispatch of Task above

#if os(iOS) || os(visionOS) || os(tvOS)
delegate.application(UIApplication.shared, didFailToRegisterForRemoteNotificationsWithError: TestError.testError)
#elseif os(watchOS)
delegate.application(WKApplication.shared(), didFailToRegisterForRemoteNotificationsWithError: TestError.testError)
#elseif os(macOS)
delegate.application(NSApplication.shared, didFailToRegisterForRemoteNotificationsWithError: TestError.testError)
#endif

try await Task.sleep(for: .milliseconds(500)) // allow dispatch of Task above

wait(for: [expectation])
XCTAssertNil(module.lastDeviceToken)
let receivedError = try XCTUnwrap(caught as? TestError)
XCTAssertEqual(receivedError, TestError.testError)
}

@MainActor
func testUnregisterNotifications() async throws {
let module = TestNotificationHandler()
let delegate = TestNotificationApplicationDelegate(module)
_ = delegate.spezi // init spezi

let action = module.unregisterRemoteNotifications
action()
}

@MainActor
func testRemoteNotificationDeliveryNoData() async throws {
let expectation = XCTestExpectation(description: "RemoteNotification")

let module = TestNotificationHandler(remoteNotificationExpectation: expectation)
let delegate = TestNotificationApplicationDelegate(module)
_ = delegate.spezi

#if os(iOS) || os(visionOS) || os(tvOS)
let result = await delegate.application(UIApplication.shared, didReceiveRemoteNotification: [:])
#elseif os(watchOS)
let result = await delegate.didReceiveRemoteNotification([:])
#elseif os(macOS)
delegate.application(NSApplication.shared, didReceiveRemoteNotification: [:])
#endif

wait(for: [expectation])
#if !os(macOS)
XCTAssertEqual(result, .noData)
#endif
}

@MainActor
func testRemoteNotificationDeliveryNewData() async throws {
let expectation = XCTestExpectation(description: "RemoteNotification")

let module = TestNotificationHandler(remoteNotificationExpectation: expectation)
#if !os(macOS)
module.setFetchResult(.newData)
#endif

let delegate = TestNotificationApplicationDelegate(module)
_ = delegate.spezi

#if os(iOS) || os(visionOS) || os(tvOS)
let result = await delegate.application(UIApplication.shared, didReceiveRemoteNotification: [:])
#elseif os(watchOS)
let result = await delegate.didReceiveRemoteNotification([:])
#elseif os(macOS)
delegate.application(NSApplication.shared, didReceiveRemoteNotification: [:])
#endif

wait(for: [expectation])
#if !os(macOS)
XCTAssertEqual(result, .newData)
#endif
}

@MainActor
func testRemoteNotificationDeliveryFailed() async throws {
let expectation = XCTestExpectation(description: "RemoteNotification")

let module = TestNotificationHandler(remoteNotificationExpectation: expectation)
#if !os(macOS)
module.setFetchResult(.failed)
#endif

let delegate = TestNotificationApplicationDelegate(module)
_ = delegate.spezi

#if os(iOS) || os(visionOS) || os(tvOS)
let result = await delegate.application(UIApplication.shared, didReceiveRemoteNotification: [:])
#elseif os(watchOS)
let result = await delegate.didReceiveRemoteNotification([:])
#elseif os(macOS)
delegate.application(NSApplication.shared, didReceiveRemoteNotification: [:])
#endif

wait(for: [expectation])
#if !os(macOS)
XCTAssertEqual(result, .failed)
#endif
}
}

0 comments on commit 3a77cf9

Please sign in to comment.