diff --git a/Sources/WebPush/Errors/MessageTooLargeError.swift b/Sources/WebPush/Errors/MessageTooLargeError.swift new file mode 100644 index 0000000..8569f13 --- /dev/null +++ b/Sources/WebPush/Errors/MessageTooLargeError.swift @@ -0,0 +1,20 @@ +// +// MessageTooLargeError.swift +// swift-webpush +// +// Created by Dimitri Bouniol on 2024-12-13. +// Copyright © 2024 Mochi Development, Inc. All rights reserved. +// + +import Foundation + +/// The message was too large, and could not be delivered to the push service. +/// +/// - SeeAlso: ``WebPushManager/maximumMessageSize`` +public struct MessageTooLargeError: LocalizedError, Hashable { + public init() {} + + public var errorDescription: String? { + "The message was too large, and could not be delivered to the push service." + } +} diff --git a/Sources/WebPush/WebPushManager.swift b/Sources/WebPush/WebPushManager.swift index 5911be0..50b1ee7 100644 --- a/Sources/WebPush/WebPushManager.swift +++ b/Sources/WebPush/WebPushManager.swift @@ -135,7 +135,7 @@ public actor WebPushManager: Sendable { } deinit { - if !didShutdown, case let .httpClient(httpClient) = executor { + if !didShutdown, case let .httpClient(httpClient, _) = executor { try? httpClient.syncShutdown() } } @@ -260,11 +260,12 @@ public actor WebPushManager: Sendable { logger: Logger? = nil ) async throws { switch executor { - case .httpClient(let httpClient): + case .httpClient(let httpClient, let privateKey): var logger = logger ?? backgroundActivityLogger - logger[metadataKey: "message"] = "\(message)" + logger[metadataKey: "message"] = ".data(\(message.base64URLEncodedString()))" try await execute( httpClient: httpClient, + applicationServerECDHPrivateKey: privateKey, data: message, subscriber: subscriber, expiration: expiration, @@ -345,9 +346,10 @@ public actor WebPushManager: Sendable { var logger = logger logger[metadataKey: "message"] = "\(message)" switch executor { - case .httpClient(let httpClient): + case .httpClient(let httpClient, let privateKey): try await execute( httpClient: httpClient, + applicationServerECDHPrivateKey: privateKey, data: message.data, subscriber: subscriber, expiration: expiration, @@ -367,6 +369,7 @@ public actor WebPushManager: Sendable { /// Send a message via HTTP Client, mocked or otherwise, encrypting it on the way. /// - Parameters: /// - httpClient: The protocol implementing HTTP-like functionality. + /// - applicationServerECDHPrivateKey: The private key to use for the key exchange. If nil, one will be generated. /// - message: The message to send as raw data. /// - subscriber: The subscriber to sign the message against. /// - expiration: The expiration of the message. @@ -374,6 +377,7 @@ public actor WebPushManager: Sendable { /// - logger: The logger to use for status updates. func execute( httpClient: some HTTPClientProtocol, + applicationServerECDHPrivateKey: P256.KeyAgreement.PrivateKey?, data message: some DataProtocol, subscriber: some SubscriberProtocol, expiration: Expiration, @@ -398,7 +402,7 @@ public actor WebPushManager: Sendable { /// Prepare authorization, private keys, and payload ahead of time to bail early if they can't be created. let authorization = try loadCurrentVAPIDAuthorizationHeader(endpoint: subscriber.endpoint, signingKey: signingKey) - let applicationServerECDHPrivateKey = P256.KeyAgreement.PrivateKey() + let applicationServerECDHPrivateKey = applicationServerECDHPrivateKey ?? P256.KeyAgreement.PrivateKey() /// Perform key exchange between the user agent's public key and our private key, deriving a shared secret. let userAgent = subscriber.userAgentKeyMaterial @@ -490,7 +494,9 @@ public actor WebPushManager: Sendable { switch response.status { case .created: break case .notFound, .gone: throw BadSubscriberError() - // TODO: 413 payload too large - log.error and throw error + case .payloadTooLarge: + logger.error("The encrypted payload was too large and was rejected by the push service.") + throw MessageTooLargeError() // TODO: 429 too many requests, 500 internal server error, 503 server shutting down - check config and perform a retry after a delay? default: throw HTTPError(response: response) } @@ -511,7 +517,7 @@ extension WebPushManager: Service { } onCancelOrGracefulShutdown: { [backgroundActivityLogger, executor] in backgroundActivityLogger.debug("Shutting down WebPushManager") do { - if case let .httpClient(httpClient) = executor { + if case let .httpClient(httpClient, _) = executor { try httpClient.syncShutdown() } } catch { @@ -675,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) @@ -701,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): @@ -715,10 +737,17 @@ extension WebPushManager { /// An internal type representing the executor for a push message. package enum Executor: Sendable { + /// Use an HTTP client and optional private key to send an encrypted payload to a subscriber. + /// + /// This is used in tests to capture the encrypted request and make sure it is well-formed. + case httpClient(any HTTPClientProtocol, P256.KeyAgreement.PrivateKey?) + /// Use an HTTP client to send an encrypted payload to a subscriber. /// /// This is used in tests to capture the encrypted request and make sure it is well-formed. - case httpClient(any HTTPClientProtocol) + package static func httpClient(_ httpClient: any HTTPClientProtocol) -> Self { + .httpClient(httpClient, nil) + } /// Use a handler to capture the original message. /// 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 abc1af2..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 { @@ -161,14 +161,16 @@ struct WebPushManagerTests { func decrypt( request: HTTPClientRequest, userAgentPrivateKey: P256.KeyAgreement.PrivateKey, - userAgentKeyMaterial: UserAgentKeyMaterial + userAgentKeyMaterial: UserAgentKeyMaterial, + expectedReadableBytes: Int = 4096, + expectedRecordSize: Int = 4010 ) async throws -> [UInt8] { var body = try #require(try await request.body?.collect(upTo: 16*1024)) - #expect(body.readableBytes == 4096) + #expect(body.readableBytes == expectedReadableBytes) let salt = body.readBytes(length: 16) let recordSize = body.readInteger(as: UInt32.self) - #expect(try #require(recordSize) == 4010) + #expect(try #require(recordSize) == expectedRecordSize) let keyIDSize = body.readInteger(as: UInt8.self) let keyID = body.readBytes(length: Int(keyIDSize ?? 0)) @@ -342,23 +344,163 @@ struct WebPushManagerTests { } } - @Test func sendMessageToNotFoundPushServerError() async throws { - await confirmation { requestWasMade in - let vapidConfiguration = VAPID.Configuration.makeTesting() + @Test func sendMessageToSubscriberWithInvalidVAPIDKey() async throws { + await confirmation(expectedCount: 0) { requestWasMade in + var subscriber = Subscriber.mockedSubscriber + subscriber.vapidKeyID = .mockedKeyID2 - let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false) + let manager = WebPushManager( + vapidConfiguration: .mockedConfiguration, + backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), + executor: .httpClient(MockHTTPClient({ request in + requestWasMade() + return HTTPClientResponse(status: .created) + })) + ) + + await #expect(throws: VAPID.ConfigurationError.matchingKeyNotFound) { + try await manager.send(string: "hello", to: subscriber) + } + } + } + + @Test(.disabled("Fails because we need a public/private key pair that fails to make a shared secret")) + func sendMessageToSubscriberWithInvalidUserAgentKey() async throws { + try await confirmation(expectedCount: 0) { requestWasMade in + let vapidConfiguration = VAPID.Configuration.mockedConfiguration + + let privateKey = try P256.KeyAgreement.PrivateKey(rawRepresentation: Data(base64Encoded: "fkqlT3FL8B34XFAmm+o6hnIfhK/nT3tB6lirzzR06I0=")!) + let publicKey = try P256.KeyAgreement.PublicKey(compressedRepresentation: Data(base64Encoded: "Ahj5uud0fNhE6YUlt8zQ2vbh0gqBiyF1qakeTq5TQ7yY")!) 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 + userAgentKeyMaterial: UserAgentKeyMaterial( + publicKey: publicKey, + authenticationSecret: Data(authenticationSecret) + ), + vapidKeyID: .mockedKeyID1 ) let manager = WebPushManager( vapidConfiguration: vapidConfiguration, backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), + executor: .httpClient( + MockHTTPClient({ request in + requestWasMade() + return HTTPClientResponse(status: .created) + }), + privateKey + ) + ) + + await #expect(throws: BadSubscriberError()) { + try await manager.send(string: "hello", to: subscriber) + } + } + } + + @Test func sendSizeLimitMessageSucceeds() async throws { + try await confirmation { requestWasMade in + let (subscriber, subscriberPrivateKey) = Subscriber.makeMockedSubscriber() + + let manager = WebPushManager( + vapidConfiguration: .mockedConfiguration, + backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), + executor: .httpClient(MockHTTPClient({ request in + try validateAuthotizationHeader( + request: request, + vapidConfiguration: .mockedConfiguration, + origin: "https://example.com" + ) + #expect(request.method == .POST) + #expect(request.headers["Content-Encoding"] == ["aes128gcm"]) + #expect(request.headers["Content-Type"] == ["application/octet-stream"]) + #expect(request.headers["TTL"] == ["2592000"]) + #expect(request.headers["Urgency"] == ["high"]) + #expect(request.headers["Topic"] == []) // TODO: Update when topic is added + + let message = try await decrypt( + request: request, + userAgentPrivateKey: subscriberPrivateKey, + userAgentKeyMaterial: subscriber.userAgentKeyMaterial, + expectedReadableBytes: 4096, + expectedRecordSize: 4010 + ) + + #expect(message == Array(repeating: 0, count: 3993)) + + requestWasMade() + return HTTPClientResponse(status: .created) + })) + ) + + try await manager.send(data: Array(repeating: 0, count: 3993), to: subscriber) + } + } + + @Test func sendExtraLargeMessageCouldSucceed() async throws { + try await confirmation { requestWasMade in + let (subscriber, subscriberPrivateKey) = Subscriber.makeMockedSubscriber() + + let manager = WebPushManager( + vapidConfiguration: .mockedConfiguration, + backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), + executor: .httpClient(MockHTTPClient({ request in + try validateAuthotizationHeader( + request: request, + vapidConfiguration: .mockedConfiguration, + origin: "https://example.com" + ) + #expect(request.method == .POST) + #expect(request.headers["Content-Encoding"] == ["aes128gcm"]) + #expect(request.headers["Content-Type"] == ["application/octet-stream"]) + #expect(request.headers["TTL"] == ["2592000"]) + #expect(request.headers["Urgency"] == ["high"]) + #expect(request.headers["Topic"] == []) // TODO: Update when topic is added + + let message = try await decrypt( + request: request, + userAgentPrivateKey: subscriberPrivateKey, + userAgentKeyMaterial: subscriber.userAgentKeyMaterial, + expectedReadableBytes: 4097, + expectedRecordSize: 4011 + ) + + #expect(message == Array(repeating: 0, count: 3994)) + + requestWasMade() + return HTTPClientResponse(status: .created) + })) + ) + + try await manager.send(data: Array(repeating: 0, count: 3994), to: subscriber) + } + } + + @Test func sendExtraLargeMessageFails() async throws { + await confirmation { requestWasMade in + let manager = WebPushManager( + vapidConfiguration: .mockedConfiguration, + backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), + executor: .httpClient(MockHTTPClient({ request in + requestWasMade() + return HTTPClientResponse(status: .payloadTooLarge) + })) + ) + + await #expect(throws: MessageTooLargeError()) { + try await manager.send(data: Array(repeating: 0, count: 3994), to: .mockedSubscriber()) + } + } + } + + @Test func sendMessageToNotFoundPushServerError() async throws { + await confirmation { requestWasMade in + let manager = WebPushManager( + vapidConfiguration: .mockedConfiguration, + backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }), executor: .httpClient(MockHTTPClient({ request in requestWasMade() return HTTPClientResponse(status: .notFound) @@ -366,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.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() @@ -395,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.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() @@ -424,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()) } } } @@ -482,4 +663,44 @@ struct WebPushManagerTests { #expect(try JSONDecoder().decode(WebPushManager.Expiration.self, from: Data("60".utf8)) == .minutes(1)) } } + + @Suite struct Urgency { + @Test func comparison() { + #expect(WebPushManager.Urgency(rawValue: "invalid") < WebPushManager.Urgency.veryLow) + #expect(WebPushManager.Urgency(rawValue: "invalid") < WebPushManager.Urgency.low) + #expect(WebPushManager.Urgency(rawValue: "invalid") < WebPushManager.Urgency.normal) + #expect(WebPushManager.Urgency(rawValue: "invalid") < WebPushManager.Urgency.high) + + #expect(WebPushManager.Urgency.veryLow < WebPushManager.Urgency.low) + #expect(WebPushManager.Urgency.veryLow < WebPushManager.Urgency.normal) + #expect(WebPushManager.Urgency.veryLow < WebPushManager.Urgency.high) + + #expect(WebPushManager.Urgency.low < WebPushManager.Urgency.normal) + #expect(WebPushManager.Urgency.low < WebPushManager.Urgency.high) + + #expect(WebPushManager.Urgency.normal < WebPushManager.Urgency.high) + } + + @Test func stringEncoding() { + #expect("\(WebPushManager.Urgency(rawValue: "future-value"))" == "future-value") + #expect("\(WebPushManager.Urgency.veryLow)" == "very-low") + #expect("\(WebPushManager.Urgency.low)" == "low") + #expect("\(WebPushManager.Urgency.normal)" == "normal") + #expect("\(WebPushManager.Urgency.high)" == "high") + } + + @Test func coding() throws { + #expect(String(decoding: try JSONEncoder().encode(WebPushManager.Urgency(rawValue: "future-value")), as: UTF8.self) == "\"future-value\"") + #expect(String(decoding: try JSONEncoder().encode(WebPushManager.Urgency.veryLow), as: UTF8.self) == "\"very-low\"") + #expect(String(decoding: try JSONEncoder().encode(WebPushManager.Urgency.low), as: UTF8.self) == "\"low\"") + #expect(String(decoding: try JSONEncoder().encode(WebPushManager.Urgency.normal), as: UTF8.self) == "\"normal\"") + #expect(String(decoding: try JSONEncoder().encode(WebPushManager.Urgency.high), as: UTF8.self) == "\"high\"") + + #expect(try JSONDecoder().decode(WebPushManager.Urgency.self, from: Data("\"future-value\"".utf8)) == .init(rawValue: "future-value")) + #expect(try JSONDecoder().decode(WebPushManager.Urgency.self, from: Data("\"very-low\"".utf8)) == .veryLow) + #expect(try JSONDecoder().decode(WebPushManager.Urgency.self, from: Data("\"low\"".utf8)) == .low) + #expect(try JSONDecoder().decode(WebPushManager.Urgency.self, from: Data("\"normal\"".utf8)) == .normal) + #expect(try JSONDecoder().decode(WebPushManager.Urgency.self, from: Data("\"high\"".utf8)) == .high) + } + } }