Skip to content

Commit

Permalink
Added testing coverage for WebPushTesting module
Browse files Browse the repository at this point in the history
  • Loading branch information
dimitribouniol committed Dec 20, 2024
1 parent 22d0b12 commit 8be059b
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 95 deletions.
16 changes: 16 additions & 0 deletions Sources/WebPush/WebPushManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<JSON: Encodable&Sendable>(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):
Expand Down
53 changes: 53 additions & 0 deletions Sources/WebPushTesting/Subscriber+Testing.swift
Original file line number Diff line number Diff line change
@@ -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"
)
}
1 change: 1 addition & 0 deletions Sources/WebPushTesting/WebPushManager+Testing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
176 changes: 81 additions & 95 deletions Tests/WebPushTests/WebPushManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Logging
import ServiceLifecycle
import Testing
@testable import WebPush
import WebPushTesting
@testable import WebPushTesting

@Suite("WebPush Manager")
struct WebPushManagerTests {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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())
}
}
}
Expand Down

0 comments on commit 8be059b

Please sign in to comment.