Skip to content

Commit 8be059b

Browse files
Added testing coverage for WebPushTesting module
1 parent 22d0b12 commit 8be059b

File tree

4 files changed

+151
-95
lines changed

4 files changed

+151
-95
lines changed

Sources/WebPush/WebPushManager.swift

+16
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,8 @@ extension WebPushManager.Urgency: Codable {
681681

682682
extension WebPushManager {
683683
/// An internal type representing a push message, accessible when using ``/WebPushTesting``.
684+
///
685+
/// - Warning: Never switch on the message type, as values may be added to it over time.
684686
public enum _Message: Sendable, CustomStringConvertible {
685687
/// A message originally sent via ``WebPushManager/send(data:to:expiration:urgency:)``
686688
case data(Data)
@@ -707,6 +709,20 @@ extension WebPushManager {
707709
}
708710
}
709711

712+
/// The string value from a ``string(_:)`` message.
713+
public var string: String? {
714+
guard case let .string(string) = self
715+
else { return nil }
716+
return string
717+
}
718+
719+
/// The json value from a ``json(_:)`` message.
720+
public func json<JSON: Encodable&Sendable>(as: JSON.Type = JSON.self) -> JSON? {
721+
guard case let .json(json) = self
722+
else { return nil }
723+
return json as? JSON
724+
}
725+
710726
public var description: String {
711727
switch self {
712728
case .data(let data):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// Subscriber+Testing.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2024-12-20.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
@preconcurrency import Crypto
10+
import Foundation
11+
import WebPush
12+
13+
extension Subscriber {
14+
/// A mocked subscriber to send messages to.
15+
public static let mockedSubscriber = Subscriber(
16+
endpoint: URL(string: "https://example.com/subscriber")!,
17+
userAgentKeyMaterial: .mockedKeyMaterial,
18+
vapidKeyID: .mockedKeyID1
19+
)
20+
21+
/// Make a mocked subscriber with a unique private key and salt.
22+
static func makeMockedSubscriber(endpoint: URL = URL(string: "https://example.com/subscriber")!) -> (subscriber: Subscriber, privateKey: P256.KeyAgreement.PrivateKey) {
23+
let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
24+
var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16)
25+
for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) }
26+
27+
let subscriber = Subscriber(
28+
endpoint: endpoint,
29+
userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)),
30+
vapidKeyID: .mockedKeyID1
31+
)
32+
33+
return (subscriber, subscriberPrivateKey)
34+
}
35+
}
36+
37+
extension SubscriberProtocol where Self == Subscriber {
38+
/// A mocked subscriber to send messages to.
39+
public static func mockedSubscriber() -> Subscriber {
40+
.mockedSubscriber
41+
}
42+
}
43+
44+
extension UserAgentKeyMaterial {
45+
/// The private key component of ``mockedKeyMaterial``.
46+
public static let mockedKeyMaterialPrivateKey = try! P256.KeyAgreement.PrivateKey(rawRepresentation: Data(base64Encoded: "BS2nTTf5wAdVvi5Om3AjSmlsCpz91XgK+uCLaIJ0T/M=")!)
47+
48+
/// A mocked user-agent-key material to attach to a subscriber.
49+
public static let mockedKeyMaterial = try! UserAgentKeyMaterial(
50+
publicKey: "BMXVxJELqTqIqMka5N8ujvW6RXI9zo_xr5BQ6XGDkrsukNVPyKRMEEfzvQGeUdeZaWAaAs2pzyv1aoHEXYMtj1M",
51+
authenticationSecret: "IzODAQZN6BbGvmm7vWQJXg"
52+
)
53+
}

Sources/WebPushTesting/WebPushManager+Testing.swift

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import WebPush
1111

1212
extension WebPushManager {
1313
/// A push message in its original form, either ``/Foundation/Data``, ``/Swift/String``, or ``/Foundation/Encodable``.
14+
/// - Warning: Never switch on the message type, as values may be added to it over time.
1415
public typealias Message = _Message
1516

1617
/// Create a mocked web push manager.

Tests/WebPushTests/WebPushManagerTests.swift

+81-95
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Logging
1313
import ServiceLifecycle
1414
import Testing
1515
@testable import WebPush
16-
import WebPushTesting
16+
@testable import WebPushTesting
1717

1818
@Suite("WebPush Manager")
1919
struct WebPushManagerTests {
@@ -346,20 +346,11 @@ struct WebPushManagerTests {
346346

347347
@Test func sendMessageToSubscriberWithInvalidVAPIDKey() async throws {
348348
await confirmation(expectedCount: 0) { requestWasMade in
349-
let vapidConfiguration = VAPID.Configuration.mockedConfiguration
350-
351-
let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
352-
var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16)
353-
for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) }
354-
355-
let subscriber = Subscriber(
356-
endpoint: URL(string: "https://example.com/subscriber")!,
357-
userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)),
358-
vapidKeyID: .mockedKeyID2
359-
)
349+
var subscriber = Subscriber.mockedSubscriber
350+
subscriber.vapidKeyID = .mockedKeyID2
360351

361352
let manager = WebPushManager(
362-
vapidConfiguration: vapidConfiguration,
353+
vapidConfiguration: .mockedConfiguration,
363354
backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }),
364355
executor: .httpClient(MockHTTPClient({ request in
365356
requestWasMade()
@@ -412,25 +403,15 @@ struct WebPushManagerTests {
412403

413404
@Test func sendSizeLimitMessageSucceeds() async throws {
414405
try await confirmation { requestWasMade in
415-
let vapidConfiguration = VAPID.Configuration.makeTesting()
416-
417-
let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
418-
var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16)
419-
for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) }
420-
421-
let subscriber = Subscriber(
422-
endpoint: URL(string: "https://example.com/subscriber")!,
423-
userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)),
424-
vapidKeyID: vapidConfiguration.primaryKey!.id
425-
)
406+
let (subscriber, subscriberPrivateKey) = Subscriber.makeMockedSubscriber()
426407

427408
let manager = WebPushManager(
428-
vapidConfiguration: vapidConfiguration,
409+
vapidConfiguration: .mockedConfiguration,
429410
backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }),
430411
executor: .httpClient(MockHTTPClient({ request in
431412
try validateAuthotizationHeader(
432413
request: request,
433-
vapidConfiguration: vapidConfiguration,
414+
vapidConfiguration: .mockedConfiguration,
434415
origin: "https://example.com"
435416
)
436417
#expect(request.method == .POST)
@@ -461,25 +442,15 @@ struct WebPushManagerTests {
461442

462443
@Test func sendExtraLargeMessageCouldSucceed() async throws {
463444
try await confirmation { requestWasMade in
464-
let vapidConfiguration = VAPID.Configuration.makeTesting()
465-
466-
let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
467-
var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16)
468-
for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) }
469-
470-
let subscriber = Subscriber(
471-
endpoint: URL(string: "https://example.com/subscriber")!,
472-
userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)),
473-
vapidKeyID: vapidConfiguration.primaryKey!.id
474-
)
445+
let (subscriber, subscriberPrivateKey) = Subscriber.makeMockedSubscriber()
475446

476447
let manager = WebPushManager(
477-
vapidConfiguration: vapidConfiguration,
448+
vapidConfiguration: .mockedConfiguration,
478449
backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }),
479450
executor: .httpClient(MockHTTPClient({ request in
480451
try validateAuthotizationHeader(
481452
request: request,
482-
vapidConfiguration: vapidConfiguration,
453+
vapidConfiguration: .mockedConfiguration,
483454
origin: "https://example.com"
484455
)
485456
#expect(request.method == .POST)
@@ -510,20 +481,8 @@ struct WebPushManagerTests {
510481

511482
@Test func sendExtraLargeMessageFails() async throws {
512483
await confirmation { requestWasMade in
513-
let vapidConfiguration = VAPID.Configuration.makeTesting()
514-
515-
let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
516-
var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16)
517-
for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) }
518-
519-
let subscriber = Subscriber(
520-
endpoint: URL(string: "https://example.com/subscriber")!,
521-
userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)),
522-
vapidKeyID: vapidConfiguration.primaryKey!.id
523-
)
524-
525484
let manager = WebPushManager(
526-
vapidConfiguration: vapidConfiguration,
485+
vapidConfiguration: .mockedConfiguration,
527486
backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }),
528487
executor: .httpClient(MockHTTPClient({ request in
529488
requestWasMade()
@@ -532,27 +491,15 @@ struct WebPushManagerTests {
532491
)
533492

534493
await #expect(throws: MessageTooLargeError()) {
535-
try await manager.send(data: Array(repeating: 0, count: 3994), to: subscriber)
494+
try await manager.send(data: Array(repeating: 0, count: 3994), to: .mockedSubscriber())
536495
}
537496
}
538497
}
539498

540499
@Test func sendMessageToNotFoundPushServerError() async throws {
541500
await confirmation { requestWasMade in
542-
let vapidConfiguration = VAPID.Configuration.mockedConfiguration
543-
544-
let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
545-
var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16)
546-
for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) }
547-
548-
let subscriber = Subscriber(
549-
endpoint: URL(string: "https://example.com/subscriber")!,
550-
userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)),
551-
vapidKeyID: .mockedKeyID1
552-
)
553-
554501
let manager = WebPushManager(
555-
vapidConfiguration: vapidConfiguration,
502+
vapidConfiguration: .mockedConfiguration,
556503
backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }),
557504
executor: .httpClient(MockHTTPClient({ request in
558505
requestWasMade()
@@ -561,27 +508,15 @@ struct WebPushManagerTests {
561508
)
562509

563510
await #expect(throws: BadSubscriberError()) {
564-
try await manager.send(string: "hello", to: subscriber)
511+
try await manager.send(string: "hello", to: .mockedSubscriber())
565512
}
566513
}
567514
}
568515

569516
@Test func sendMessageToGonePushServerError() async throws {
570517
await confirmation { requestWasMade in
571-
let vapidConfiguration = VAPID.Configuration.mockedConfiguration
572-
573-
let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
574-
var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16)
575-
for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) }
576-
577-
let subscriber = Subscriber(
578-
endpoint: URL(string: "https://example.com/subscriber")!,
579-
userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)),
580-
vapidKeyID: .mockedKeyID1
581-
)
582-
583518
let manager = WebPushManager(
584-
vapidConfiguration: vapidConfiguration,
519+
vapidConfiguration: .mockedConfiguration,
585520
backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }),
586521
executor: .httpClient(MockHTTPClient({ request in
587522
requestWasMade()
@@ -590,27 +525,15 @@ struct WebPushManagerTests {
590525
)
591526

592527
await #expect(throws: BadSubscriberError()) {
593-
try await manager.send(string: "hello", to: subscriber)
528+
try await manager.send(string: "hello", to: .mockedSubscriber())
594529
}
595530
}
596531
}
597532

598533
@Test func sendMessageToUnknownPushServerError() async throws {
599534
await confirmation { requestWasMade in
600-
let vapidConfiguration = VAPID.Configuration.mockedConfiguration
601-
602-
let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
603-
var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16)
604-
for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) }
605-
606-
let subscriber = Subscriber(
607-
endpoint: URL(string: "https://example.com/subscriber")!,
608-
userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)),
609-
vapidKeyID: .mockedKeyID1
610-
)
611-
612535
let manager = WebPushManager(
613-
vapidConfiguration: vapidConfiguration,
536+
vapidConfiguration: .mockedConfiguration,
614537
backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }),
615538
executor: .httpClient(MockHTTPClient({ request in
616539
requestWasMade()
@@ -619,7 +542,70 @@ struct WebPushManagerTests {
619542
)
620543

621544
await #expect(throws: HTTPError.self) {
622-
try await manager.send(string: "hello", to: subscriber)
545+
try await manager.send(string: "hello", to: .mockedSubscriber())
546+
}
547+
}
548+
}
549+
}
550+
551+
@Suite("Sending Mocked Messages")
552+
struct SendingMockedMessages {
553+
@Test func sendSuccessfulTextMessage() async throws {
554+
try await confirmation { requestWasMade in
555+
let manager = WebPushManager.makeMockedManager(backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) })) { message, subscriber, expiration, urgency in
556+
#expect(message.string == "hello")
557+
#expect(subscriber.endpoint.absoluteString == "https://example.com/subscriber")
558+
#expect(subscriber.vapidKeyID == .mockedKeyID1)
559+
#expect(expiration == .recommendedMaximum)
560+
#expect(urgency == .high)
561+
requestWasMade()
562+
}
563+
564+
try await manager.send(string: "hello", to: .mockedSubscriber())
565+
}
566+
}
567+
568+
@Test func sendSuccessfulDataMessage() async throws {
569+
try await confirmation { requestWasMade in
570+
let manager = WebPushManager.makeMockedManager(backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) })) { message, subscriber, expiration, urgency in
571+
try #expect(message.data == Data("hello".utf8Bytes))
572+
#expect(subscriber.endpoint.absoluteString == "https://example.com/subscriber")
573+
#expect(subscriber.vapidKeyID == .mockedKeyID1)
574+
#expect(expiration == .recommendedMaximum)
575+
#expect(urgency == .high)
576+
requestWasMade()
577+
}
578+
579+
try await manager.send(data: "hello".utf8Bytes, to: .mockedSubscriber())
580+
}
581+
}
582+
583+
@Test func sendSuccessfulJSONMessage() async throws {
584+
try await confirmation { requestWasMade in
585+
let manager = WebPushManager.makeMockedManager(backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) })) { message, subscriber, expiration, urgency in
586+
#expect(message.json() == ["hello" : "world"])
587+
#expect(subscriber.endpoint.absoluteString == "https://example.com/subscriber")
588+
#expect(subscriber.vapidKeyID == .mockedKeyID1)
589+
#expect(expiration == .recommendedMaximum)
590+
#expect(urgency == .high)
591+
requestWasMade()
592+
}
593+
594+
try await manager.send(json: ["hello" : "world"], to: .mockedSubscriber())
595+
}
596+
}
597+
598+
@Test func sendPropagatedMockedFailure() async throws {
599+
await confirmation { requestWasMade in
600+
struct CustomError: Error {}
601+
602+
let manager = WebPushManager.makeMockedManager(backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) })) { _, _, _, _ in
603+
requestWasMade()
604+
throw CustomError()
605+
}
606+
607+
await #expect(throws: CustomError.self) {
608+
try await manager.send(data: Array(repeating: 0, count: 3994), to: .mockedSubscriber())
623609
}
624610
}
625611
}

0 commit comments

Comments
 (0)