diff --git a/Sources/WebPush/WebPushManager.swift b/Sources/WebPush/WebPushManager.swift index 37c6cbb..50b1ee7 100644 --- a/Sources/WebPush/WebPushManager.swift +++ b/Sources/WebPush/WebPushManager.swift @@ -681,6 +681,8 @@ extension WebPushManager.Urgency: Codable { extension WebPushManager { /// An internal type representing a push message, accessible when using ``/WebPushTesting``. + /// + /// - Warning: Never switch on the message type, as values may be added to it over time. public enum _Message: Sendable, CustomStringConvertible { /// A message originally sent via ``WebPushManager/send(data:to:expiration:urgency:)`` case data(Data) @@ -707,6 +709,20 @@ extension WebPushManager { } } + /// The string value from a ``string(_:)`` message. + public var string: String? { + guard case let .string(string) = self + else { return nil } + return string + } + + /// The json value from a ``json(_:)`` message. + public func json(as: JSON.Type = JSON.self) -> JSON? { + guard case let .json(json) = self + else { return nil } + return json as? JSON + } + public var description: String { switch self { case .data(let data): diff --git a/Sources/WebPushTesting/Subscriber+Testing.swift b/Sources/WebPushTesting/Subscriber+Testing.swift new file mode 100644 index 0000000..b86df66 --- /dev/null +++ b/Sources/WebPushTesting/Subscriber+Testing.swift @@ -0,0 +1,53 @@ +// +// Subscriber+Testing.swift +// swift-webpush +// +// Created by Dimitri Bouniol on 2024-12-20. +// Copyright © 2024 Mochi Development, Inc. All rights reserved. +// + +@preconcurrency import Crypto +import Foundation +import WebPush + +extension Subscriber { + /// A mocked subscriber to send messages to. + public static let mockedSubscriber = Subscriber( + endpoint: URL(string: "https://example.com/subscriber")!, + userAgentKeyMaterial: .mockedKeyMaterial, + vapidKeyID: .mockedKeyID1 + ) + + /// Make a mocked subscriber with a unique private key and salt. + static func makeMockedSubscriber(endpoint: URL = URL(string: "https://example.com/subscriber")!) -> (subscriber: Subscriber, privateKey: P256.KeyAgreement.PrivateKey) { + let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false) + var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16) + for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) } + + let subscriber = Subscriber( + endpoint: endpoint, + userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)), + vapidKeyID: .mockedKeyID1 + ) + + return (subscriber, subscriberPrivateKey) + } +} + +extension SubscriberProtocol where Self == Subscriber { + /// A mocked subscriber to send messages to. + public static func mockedSubscriber() -> Subscriber { + .mockedSubscriber + } +} + +extension UserAgentKeyMaterial { + /// The private key component of ``mockedKeyMaterial``. + public static let mockedKeyMaterialPrivateKey = try! P256.KeyAgreement.PrivateKey(rawRepresentation: Data(base64Encoded: "BS2nTTf5wAdVvi5Om3AjSmlsCpz91XgK+uCLaIJ0T/M=")!) + + /// A mocked user-agent-key material to attach to a subscriber. + public static let mockedKeyMaterial = try! UserAgentKeyMaterial( + publicKey: "BMXVxJELqTqIqMka5N8ujvW6RXI9zo_xr5BQ6XGDkrsukNVPyKRMEEfzvQGeUdeZaWAaAs2pzyv1aoHEXYMtj1M", + authenticationSecret: "IzODAQZN6BbGvmm7vWQJXg" + ) +} diff --git a/Sources/WebPushTesting/WebPushManager+Testing.swift b/Sources/WebPushTesting/WebPushManager+Testing.swift index 1cd6979..0243ea0 100644 --- a/Sources/WebPushTesting/WebPushManager+Testing.swift +++ b/Sources/WebPushTesting/WebPushManager+Testing.swift @@ -11,6 +11,7 @@ import WebPush extension WebPushManager { /// A push message in its original form, either ``/Foundation/Data``, ``/Swift/String``, or ``/Foundation/Encodable``. + /// - Warning: Never switch on the message type, as values may be added to it over time. public typealias Message = _Message /// Create a mocked web push manager. diff --git a/Tests/WebPushTests/WebPushManagerTests.swift b/Tests/WebPushTests/WebPushManagerTests.swift index 3630eb5..86e3afe 100644 --- a/Tests/WebPushTests/WebPushManagerTests.swift +++ b/Tests/WebPushTests/WebPushManagerTests.swift @@ -13,7 +13,7 @@ import Logging import ServiceLifecycle import Testing @testable import WebPush -import WebPushTesting +@testable import WebPushTesting @Suite("WebPush Manager") struct WebPushManagerTests { @@ -346,20 +346,11 @@ struct WebPushManagerTests { @Test func sendMessageToSubscriberWithInvalidVAPIDKey() async throws { await confirmation(expectedCount: 0) { requestWasMade in - let vapidConfiguration = VAPID.Configuration.mockedConfiguration - - let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false) - var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16) - for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) } - - let subscriber = Subscriber( - endpoint: URL(string: "https://example.com/subscriber")!, - userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)), - vapidKeyID: .mockedKeyID2 - ) + var subscriber = Subscriber.mockedSubscriber + subscriber.vapidKeyID = .mockedKeyID2 let manager = WebPushManager( - vapidConfiguration: vapidConfiguration, + vapidConfiguration: .mockedConfiguration, backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), executor: .httpClient(MockHTTPClient({ request in requestWasMade() @@ -412,25 +403,15 @@ struct WebPushManagerTests { @Test func sendSizeLimitMessageSucceeds() async throws { try await confirmation { requestWasMade in - let vapidConfiguration = VAPID.Configuration.makeTesting() - - let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false) - var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16) - for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) } - - let subscriber = Subscriber( - endpoint: URL(string: "https://example.com/subscriber")!, - userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)), - vapidKeyID: vapidConfiguration.primaryKey!.id - ) + let (subscriber, subscriberPrivateKey) = Subscriber.makeMockedSubscriber() let manager = WebPushManager( - vapidConfiguration: vapidConfiguration, + vapidConfiguration: .mockedConfiguration, backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), executor: .httpClient(MockHTTPClient({ request in try validateAuthotizationHeader( request: request, - vapidConfiguration: vapidConfiguration, + vapidConfiguration: .mockedConfiguration, origin: "https://example.com" ) #expect(request.method == .POST) @@ -461,25 +442,15 @@ struct WebPushManagerTests { @Test func sendExtraLargeMessageCouldSucceed() async throws { try await confirmation { requestWasMade in - let vapidConfiguration = VAPID.Configuration.makeTesting() - - let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false) - var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16) - for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) } - - let subscriber = Subscriber( - endpoint: URL(string: "https://example.com/subscriber")!, - userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)), - vapidKeyID: vapidConfiguration.primaryKey!.id - ) + let (subscriber, subscriberPrivateKey) = Subscriber.makeMockedSubscriber() let manager = WebPushManager( - vapidConfiguration: vapidConfiguration, + vapidConfiguration: .mockedConfiguration, backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), executor: .httpClient(MockHTTPClient({ request in try validateAuthotizationHeader( request: request, - vapidConfiguration: vapidConfiguration, + vapidConfiguration: .mockedConfiguration, origin: "https://example.com" ) #expect(request.method == .POST) @@ -510,20 +481,8 @@ struct WebPushManagerTests { @Test func sendExtraLargeMessageFails() async throws { await confirmation { requestWasMade in - let vapidConfiguration = VAPID.Configuration.makeTesting() - - let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false) - var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16) - for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) } - - let subscriber = Subscriber( - endpoint: URL(string: "https://example.com/subscriber")!, - userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)), - vapidKeyID: vapidConfiguration.primaryKey!.id - ) - let manager = WebPushManager( - vapidConfiguration: vapidConfiguration, + vapidConfiguration: .mockedConfiguration, backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), executor: .httpClient(MockHTTPClient({ request in requestWasMade() @@ -532,27 +491,15 @@ struct WebPushManagerTests { ) await #expect(throws: MessageTooLargeError()) { - try await manager.send(data: Array(repeating: 0, count: 3994), to: subscriber) + try await manager.send(data: Array(repeating: 0, count: 3994), to: .mockedSubscriber()) } } } @Test func sendMessageToNotFoundPushServerError() async throws { await confirmation { requestWasMade in - let vapidConfiguration = VAPID.Configuration.mockedConfiguration - - let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false) - var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16) - for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) } - - let subscriber = Subscriber( - endpoint: URL(string: "https://example.com/subscriber")!, - userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)), - vapidKeyID: .mockedKeyID1 - ) - let manager = WebPushManager( - vapidConfiguration: vapidConfiguration, + vapidConfiguration: .mockedConfiguration, backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), executor: .httpClient(MockHTTPClient({ request in requestWasMade() @@ -561,27 +508,15 @@ struct WebPushManagerTests { ) await #expect(throws: BadSubscriberError()) { - try await manager.send(string: "hello", to: subscriber) + try await manager.send(string: "hello", to: .mockedSubscriber()) } } } @Test func sendMessageToGonePushServerError() async throws { await confirmation { requestWasMade in - let vapidConfiguration = VAPID.Configuration.mockedConfiguration - - let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false) - var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16) - for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) } - - let subscriber = Subscriber( - endpoint: URL(string: "https://example.com/subscriber")!, - userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)), - vapidKeyID: .mockedKeyID1 - ) - let manager = WebPushManager( - vapidConfiguration: vapidConfiguration, + vapidConfiguration: .mockedConfiguration, backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), executor: .httpClient(MockHTTPClient({ request in requestWasMade() @@ -590,27 +525,15 @@ struct WebPushManagerTests { ) await #expect(throws: BadSubscriberError()) { - try await manager.send(string: "hello", to: subscriber) + try await manager.send(string: "hello", to: .mockedSubscriber()) } } } @Test func sendMessageToUnknownPushServerError() async throws { await confirmation { requestWasMade in - let vapidConfiguration = VAPID.Configuration.mockedConfiguration - - let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false) - var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16) - for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) } - - let subscriber = Subscriber( - endpoint: URL(string: "https://example.com/subscriber")!, - userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)), - vapidKeyID: .mockedKeyID1 - ) - let manager = WebPushManager( - vapidConfiguration: vapidConfiguration, + vapidConfiguration: .mockedConfiguration, backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), executor: .httpClient(MockHTTPClient({ request in requestWasMade() @@ -619,7 +542,70 @@ struct WebPushManagerTests { ) await #expect(throws: HTTPError.self) { - try await manager.send(string: "hello", to: subscriber) + try await manager.send(string: "hello", to: .mockedSubscriber()) + } + } + } + } + + @Suite("Sending Mocked Messages") + struct SendingMockedMessages { + @Test func sendSuccessfulTextMessage() async throws { + try await confirmation { requestWasMade in + let manager = WebPushManager.makeMockedManager(backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) })) { message, subscriber, expiration, urgency in + #expect(message.string == "hello") + #expect(subscriber.endpoint.absoluteString == "https://example.com/subscriber") + #expect(subscriber.vapidKeyID == .mockedKeyID1) + #expect(expiration == .recommendedMaximum) + #expect(urgency == .high) + requestWasMade() + } + + try await manager.send(string: "hello", to: .mockedSubscriber()) + } + } + + @Test func sendSuccessfulDataMessage() async throws { + try await confirmation { requestWasMade in + let manager = WebPushManager.makeMockedManager(backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) })) { message, subscriber, expiration, urgency in + try #expect(message.data == Data("hello".utf8Bytes)) + #expect(subscriber.endpoint.absoluteString == "https://example.com/subscriber") + #expect(subscriber.vapidKeyID == .mockedKeyID1) + #expect(expiration == .recommendedMaximum) + #expect(urgency == .high) + requestWasMade() + } + + try await manager.send(data: "hello".utf8Bytes, to: .mockedSubscriber()) + } + } + + @Test func sendSuccessfulJSONMessage() async throws { + try await confirmation { requestWasMade in + let manager = WebPushManager.makeMockedManager(backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) })) { message, subscriber, expiration, urgency in + #expect(message.json() == ["hello" : "world"]) + #expect(subscriber.endpoint.absoluteString == "https://example.com/subscriber") + #expect(subscriber.vapidKeyID == .mockedKeyID1) + #expect(expiration == .recommendedMaximum) + #expect(urgency == .high) + requestWasMade() + } + + try await manager.send(json: ["hello" : "world"], to: .mockedSubscriber()) + } + } + + @Test func sendPropagatedMockedFailure() async throws { + await confirmation { requestWasMade in + struct CustomError: Error {} + + let manager = WebPushManager.makeMockedManager(backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) })) { _, _, _, _ in + requestWasMade() + throw CustomError() + } + + await #expect(throws: CustomError.self) { + try await manager.send(data: Array(repeating: 0, count: 3994), to: .mockedSubscriber()) } } }