From 2b2ec1f7c75cfd3887ee8359a83e8dbd3e8c9a8c Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 15 Dec 2024 03:32:50 -0800 Subject: [PATCH 1/3] Improved URL decoding to inline as needed and remove duplicate code --- .../WebPush/Helpers/DataProtocol+Base64URLCoding.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/WebPush/Helpers/DataProtocol+Base64URLCoding.swift b/Sources/WebPush/Helpers/DataProtocol+Base64URLCoding.swift index fb80ae1..835ddbc 100644 --- a/Sources/WebPush/Helpers/DataProtocol+Base64URLCoding.swift +++ b/Sources/WebPush/Helpers/DataProtocol+Base64URLCoding.swift @@ -10,6 +10,7 @@ import Foundation extension DataProtocol { @_disfavoredOverload + @usableFromInline func base64URLEncodedString() -> String { Data(self) .base64EncodedString() @@ -20,18 +21,16 @@ extension DataProtocol { } extension ContiguousBytes { + @usableFromInline func base64URLEncodedString() -> String { withUnsafeBytes { bytes in - Data(bytes) - .base64EncodedString() - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "=", with: "") + (bytes as any DataProtocol).base64URLEncodedString() } } } extension DataProtocol where Self: RangeReplaceableCollection { + @usableFromInline init?(base64URLEncoded string: some StringProtocol) { var base64String = string.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") while base64String.count % 4 != 0 { From a8bcb64876d96d88e373beb458b1276853ea9256 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 15 Dec 2024 03:39:19 -0800 Subject: [PATCH 2/3] Added initializer to VAPID.Key for decoding Base64 strings directly. --- README.md | 10 ++++++++-- Sources/WebPush/VAPID/VAPIDKey.swift | 13 +++++++++++++ vapid-key-generator/Sources/VAPIDKeyGenerator.swift | 6 +++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 153f732..9bc18d4 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Please check the [releases](https://github.com/mochidev/swift-webpush/releases) dependencies: [ .package( url: "https://github.com/mochidev/swift-webPush.git", - .upToNextMinor(from: "0.1.3") + .upToNextMinor(from: "0.1.4") ), ], ... @@ -105,7 +105,13 @@ Before integrating WebPush into your server, you must generate one time VAPID ke To uninstall the generator: ```zsh -% package experimental-uninstall vapid-key-generator +% swift package experimental-uninstall vapid-key-generator +``` + +To update the generator, uninstall it and re-install it after pulling from main: +```zsh +% swift package experimental-uninstall vapid-key-generator +% swift package experimental-install ``` Once installed, a new configuration can be generated as needed: diff --git a/Sources/WebPush/VAPID/VAPIDKey.swift b/Sources/WebPush/VAPID/VAPIDKey.swift index 1a52451..2db9d81 100644 --- a/Sources/WebPush/VAPID/VAPIDKey.swift +++ b/Sources/WebPush/VAPID/VAPIDKey.swift @@ -16,13 +16,26 @@ extension VoluntaryApplicationServerIdentification { public struct Key: Sendable { private var privateKey: P256.Signing.PrivateKey + /// Create a brand new VAPID signing key. + /// + /// - Note: You must persist this key somehow if you are creating it yourself. public init() { privateKey = P256.Signing.PrivateKey(compactRepresentable: false) } + /// Initialize a key from a P256 SIgning Private Key. + /// + /// - Warning: Do not re-use this key for any other purpose other than VAPID authorization! public init(privateKey: P256.Signing.PrivateKey) { self.privateKey = privateKey } + + /// Decode a key directly from a Base 64 (URL) encoded string, or throw an error if decoding failed. + public init(base64URLEncoded: String) throws { + guard let data = Data(base64URLEncoded: base64URLEncoded) + else { throw Base64URLDecodingError() } + privateKey = try P256.Signing.PrivateKey(rawRepresentation: data) + } } } diff --git a/vapid-key-generator/Sources/VAPIDKeyGenerator.swift b/vapid-key-generator/Sources/VAPIDKeyGenerator.swift index 0b99229..688c9ae 100644 --- a/vapid-key-generator/Sources/VAPIDKeyGenerator.swift +++ b/vapid-key-generator/Sources/VAPIDKeyGenerator.swift @@ -53,11 +53,11 @@ struct MyCoolerTool: ParsableCommand { if silent { print("\(json)") } else { - print("VAPID.Key (with quotes): \(json)\n\n") + print("VAPID.Key: \(json)\n\n") print("Example Usage:") print(" // TODO: Load this data from .env or from file system") - print(" let keyData = Data(#\" \(json) \"#.utf8)") - print(" let vapidKey = try JSONDecoder().decode(VAPID.Key.self, from: keyData)") + print(" let keyData = Data(\(json).utf8)") + print(" let vapidKey = try VAPID.Key(base64URLEncoded: keyData)") } } else if !keyOnly { let contactInformation = if let supportURL, email == nil { From 06962c3386cec433f4fab7fe48cd3f352a12ee46 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 15 Dec 2024 03:39:43 -0800 Subject: [PATCH 3/3] Improved code coverage from 34% to 50% --- Tests/WebPushTests/Base64URLCodingTests.swift | 32 +- .../VAPIDConfigurationTests.swift | 304 ++++++++++++++++++ Tests/WebPushTests/VAPIDTokenTests.swift | 31 ++ 3 files changed, 356 insertions(+), 11 deletions(-) create mode 100644 Tests/WebPushTests/VAPIDConfigurationTests.swift diff --git a/Tests/WebPushTests/Base64URLCodingTests.swift b/Tests/WebPushTests/Base64URLCodingTests.swift index 759ceec..e547a87 100644 --- a/Tests/WebPushTests/Base64URLCodingTests.swift +++ b/Tests/WebPushTests/Base64URLCodingTests.swift @@ -10,16 +10,26 @@ import Foundation import Testing @testable import WebPush -@Test func base64URLDecoding() async throws { - let string = ">>> Hello, swift-webpush world??? 🎉" - let base64Encoded = "Pj4+IEhlbGxvLCBzd2lmdC13ZWJwdXNoIHdvcmxkPz8/IPCfjok=" - let base64URLEncoded = "Pj4-IEhlbGxvLCBzd2lmdC13ZWJwdXNoIHdvcmxkPz8_IPCfjok" - #expect(String(decoding: Data(base64URLEncoded: base64Encoded)!, as: UTF8.self) == string) - #expect(String(decoding: Data(base64URLEncoded: base64URLEncoded)!, as: UTF8.self) == string) -} +@Suite("Base 64 URL Coding") +struct Base64URLCoding { + @Test func base64URLDecoding() async throws { + let string = ">>> Hello, swift-webpush world??? 🎉" + let base64Encoded = "Pj4+IEhlbGxvLCBzd2lmdC13ZWJwdXNoIHdvcmxkPz8/IPCfjok=" + let base64URLEncoded = "Pj4-IEhlbGxvLCBzd2lmdC13ZWJwdXNoIHdvcmxkPz8_IPCfjok" + #expect(String(decoding: Data(base64URLEncoded: base64Encoded)!, as: UTF8.self) == string) + #expect(String(decoding: Data(base64URLEncoded: base64URLEncoded)!, as: UTF8.self) == string) + #expect(String(decoding: [UInt8](base64URLEncoded: base64Encoded)!, as: UTF8.self) == string) + #expect(String(decoding: [UInt8](base64URLEncoded: base64URLEncoded)!, as: UTF8.self) == string) + } + + @Test func invalidBase64URLDecoding() async throws { + #expect(Data(base64URLEncoded: " ") == nil) + } -@Test func base64URLEncoding() async throws { - let string = ">>> Hello, swift-webpush world??? 🎉" - let base64URLEncoded = "Pj4-IEhlbGxvLCBzd2lmdC13ZWJwdXNoIHdvcmxkPz8_IPCfjok" - #expect(Array(string.utf8).base64URLEncodedString() == base64URLEncoded) + @Test func base64URLEncoding() async throws { + let string = ">>> Hello, swift-webpush world??? 🎉" + let base64URLEncoded = "Pj4-IEhlbGxvLCBzd2lmdC13ZWJwdXNoIHdvcmxkPz8_IPCfjok" + #expect([UInt8](string.utf8).base64URLEncodedString() == base64URLEncoded) + #expect(Data(string.utf8).base64URLEncodedString() == base64URLEncoded) + } } diff --git a/Tests/WebPushTests/VAPIDConfigurationTests.swift b/Tests/WebPushTests/VAPIDConfigurationTests.swift new file mode 100644 index 0000000..6e60b62 --- /dev/null +++ b/Tests/WebPushTests/VAPIDConfigurationTests.swift @@ -0,0 +1,304 @@ +// +// VAPIDConfigurationTests.swift +// swift-webpush +// +// Created by Dimitri Bouniol on 2024-12-15. +// Copyright © 2024 Mochi Development, Inc. All rights reserved. +// + +import Crypto +import Foundation +import Testing +@testable import WebPush + +@Suite("VAPID Configuration Tests") +struct VAPIDConfigurationTests { + @Suite + struct Initialization { + let key1 = try! VAPID.Key(base64URLEncoded: "FniTgSrf0l+BdfeC6LiblKXBbY4LQm0S+4STNCoJI+0=") + let key2 = try! VAPID.Key(base64URLEncoded: "wyQaGWNwvXKzVmPIhkqVQvQ+FKx1SNqHJ+re8n2ORrk=") + let key3 = try! VAPID.Key(base64URLEncoded: "bcZgo/p2WFqXaKFzmYaDKO/gARjWvGi3oXyHM2QNlfE=") + + @Test func primaryKeyOnly() { + let config = VAPID.Configuration( + key: key1, + contactInformation: .email("test@email.com") + ) + #expect(config.primaryKey == key1) + #expect(config.keys == [key1]) + #expect(config.deprecatedKeys == nil) + #expect(config.contactInformation == .email("test@email.com")) + #expect(config.expirationDuration == .hours(22)) + #expect(config.validityDuration == .hours(20)) + } + + @Test func emptyDeprecatedKeys() { + let config = VAPID.Configuration( + key: key1, + deprecatedKeys: [], + contactInformation: .url(URL(string: "https://example.com")!), + expirationDuration: .hours(24), + validityDuration: .hours(12) + ) + #expect(config.primaryKey == key1) + #expect(config.keys == [key1]) + #expect(config.deprecatedKeys == nil) + #expect(config.contactInformation == .url(URL(string: "https://example.com")!)) + #expect(config.expirationDuration == .hours(24)) + #expect(config.validityDuration == .hours(12)) + } + + @Test func deprecatedKeys() { + let config = VAPID.Configuration( + key: key1, + deprecatedKeys: [key2, key3], + contactInformation: .email("test@email.com") + ) + #expect(config.primaryKey == key1) + #expect(config.keys == [key1]) + #expect(config.deprecatedKeys == [key2, key3]) + #expect(config.contactInformation == .email("test@email.com")) + #expect(config.expirationDuration == .hours(22)) + #expect(config.validityDuration == .hours(20)) + } + + @Test func deprecatedAndPrimaryKeys() { + let config = VAPID.Configuration( + key: key1, + deprecatedKeys: [key2, key3, key1], + contactInformation: .url(URL(string: "https://example.com")!), + expirationDuration: .hours(24), + validityDuration: .hours(12) + ) + #expect(config.primaryKey == key1) + #expect(config.keys == [key1]) + #expect(config.deprecatedKeys == [key2, key3]) + #expect(config.contactInformation == .url(URL(string: "https://example.com")!)) + #expect(config.expirationDuration == .hours(24)) + #expect(config.validityDuration == .hours(12)) + } + + @Test func multipleKeys() throws { + let config = try VAPID.Configuration( + primaryKey: nil, + keys: [key1, key2], + deprecatedKeys: nil, + contactInformation: .email("test@email.com") + ) + #expect(config.primaryKey == nil) + #expect(config.keys == [key1, key2]) + #expect(config.deprecatedKeys == nil) + #expect(config.contactInformation == .email("test@email.com")) + #expect(config.expirationDuration == .hours(22)) + #expect(config.validityDuration == .hours(20)) + } + + @Test func noKeys() throws { + #expect(throws: VAPID.ConfigurationError.keysNotProvided) { + try VAPID.Configuration( + primaryKey: nil, + keys: [], + deprecatedKeys: [key2, key3], + contactInformation: .email("test@email.com") + ) + } + } + + @Test func multipleAndDeprecatedKeys() throws { + let config = try VAPID.Configuration( + primaryKey: nil, + keys: [key1, key2], + deprecatedKeys: [key2], + contactInformation: .email("test@email.com") + ) + #expect(config.primaryKey == nil) + #expect(config.keys == [key1, key2]) + #expect(config.deprecatedKeys == nil) + #expect(config.contactInformation == .email("test@email.com")) + #expect(config.expirationDuration == .hours(22)) + #expect(config.validityDuration == .hours(20)) + } + + @Test func multipleAndPrimaryKeys() throws { + let config = try VAPID.Configuration( + primaryKey: key1, + keys: [key2], + deprecatedKeys: [key2, key3, key1], + contactInformation: .url(URL(string: "https://example.com")!), + expirationDuration: .hours(24), + validityDuration: .hours(12) + ) + #expect(config.primaryKey == key1) + #expect(config.keys == [key1, key2]) + #expect(config.deprecatedKeys == [key3]) + #expect(config.contactInformation == .url(URL(string: "https://example.com")!)) + #expect(config.expirationDuration == .hours(24)) + #expect(config.validityDuration == .hours(12)) + } + } + + @Suite + struct Updates { + let key1 = try! VAPID.Key(base64URLEncoded: "FniTgSrf0l+BdfeC6LiblKXBbY4LQm0S+4STNCoJI+0=") + let key2 = try! VAPID.Key(base64URLEncoded: "wyQaGWNwvXKzVmPIhkqVQvQ+FKx1SNqHJ+re8n2ORrk=") + let key3 = try! VAPID.Key(base64URLEncoded: "bcZgo/p2WFqXaKFzmYaDKO/gARjWvGi3oXyHM2QNlfE=") + + @Test func primaryKeyOnly() throws { + var config = VAPID.Configuration(key: key1, contactInformation: .email("test@email.com")) + + try config.updateKeys(primaryKey: key2, keys: [], deprecatedKeys: nil) + #expect(config.primaryKey == key2) + #expect(config.keys == [key2]) + #expect(config.deprecatedKeys == nil) + } + + @Test func noKeys() throws { + var config = VAPID.Configuration(key: key1, contactInformation: .email("test@email.com")) + #expect(throws: VAPID.ConfigurationError.keysNotProvided) { + try config.updateKeys(primaryKey: nil, keys: [], deprecatedKeys: nil) + } + #expect(throws: VAPID.ConfigurationError.keysNotProvided) { + try config.updateKeys(primaryKey: nil, keys: [], deprecatedKeys: []) + } + #expect(throws: VAPID.ConfigurationError.keysNotProvided) { + try config.updateKeys(primaryKey: nil, keys: [], deprecatedKeys: [key1]) + } + } + + @Test func multipleKeys() throws { + var config = VAPID.Configuration(key: key1, contactInformation: .email("test@email.com")) + + try config.updateKeys(primaryKey: nil, keys: [key2], deprecatedKeys: nil) + #expect(config.primaryKey == nil) + #expect(config.keys == [key2]) + #expect(config.deprecatedKeys == nil) + + try config.updateKeys(primaryKey: nil, keys: [key2, key3], deprecatedKeys: nil) + #expect(config.primaryKey == nil) + #expect(config.keys == [key2, key3]) + #expect(config.deprecatedKeys == nil) + } + + @Test func multipleAndDeprecatedKeys() throws { + var config = VAPID.Configuration(key: key1, contactInformation: .email("test@email.com")) + + try config.updateKeys(primaryKey: nil, keys: [key2], deprecatedKeys: [key2, key3]) + #expect(config.primaryKey == nil) + #expect(config.keys == [key2]) + #expect(config.deprecatedKeys == [key3]) + + try config.updateKeys(primaryKey: nil, keys: [key2, key3], deprecatedKeys: [key2, key3]) + #expect(config.primaryKey == nil) + #expect(config.keys == [key2, key3]) + #expect(config.deprecatedKeys == nil) + } + + @Test func multipleAndPrimaryKeys() throws { + var config = VAPID.Configuration(key: key1, contactInformation: .email("test@email.com")) + + try config.updateKeys(primaryKey: key2, keys: [key3], deprecatedKeys: [key1, key2, key3]) + #expect(config.primaryKey == key2) + #expect(config.keys == [key2, key3]) + #expect(config.deprecatedKeys == [key1]) + + try config.updateKeys(primaryKey: key2, keys: [key3], deprecatedKeys: [key2, key3]) + #expect(config.primaryKey == key2) + #expect(config.keys == [key2, key3]) + #expect(config.deprecatedKeys == nil) + } + } + + @Suite + struct Coding { + let key1 = try! VAPID.Key(base64URLEncoded: "FniTgSrf0l+BdfeC6LiblKXBbY4LQm0S+4STNCoJI+0=") + let key2 = try! VAPID.Key(base64URLEncoded: "wyQaGWNwvXKzVmPIhkqVQvQ+FKx1SNqHJ+re8n2ORrk=") + let key3 = try! VAPID.Key(base64URLEncoded: "bcZgo/p2WFqXaKFzmYaDKO/gARjWvGi3oXyHM2QNlfE=") + + func encode(_ configuration: VAPID.Configuration) throws -> String { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] + return String(decoding: try encoder.encode(configuration), as: UTF8.self) + } + + @Test func encodesPrimaryKeyOnly() async throws { + #expect( + try encode(.init(key: key1, contactInformation: .email("test@example.com"))) == + """ + { + "contactInformation" : "mailto:test@example.com", + "expirationDuration" : 79200, + "primaryKey" : "FniTgSrf0l+BdfeC6LiblKXBbY4LQm0S+4STNCoJI+0=", + "validityDuration" : 72000 + } + """ + ) + } + + @Test func encodesMultipleKeysWithoutDuplicates() async throws { + #expect( + try encode(.init( + primaryKey: key1, + keys: [key2], + deprecatedKeys: [key1, key2, key3], + contactInformation: .email("test@example.com"), + expirationDuration: .hours(1), + validityDuration: .hours(10) + )) == + """ + { + "contactInformation" : "mailto:test@example.com", + "deprecatedKeys" : [ + "bcZgo/p2WFqXaKFzmYaDKO/gARjWvGi3oXyHM2QNlfE=" + ], + "expirationDuration" : 3600, + "keys" : [ + "wyQaGWNwvXKzVmPIhkqVQvQ+FKx1SNqHJ+re8n2ORrk=" + ], + "primaryKey" : "FniTgSrf0l+BdfeC6LiblKXBbY4LQm0S+4STNCoJI+0=", + "validityDuration" : 36000 + } + """ + ) + } + } +} + +@Suite("Contact Information Coding") +struct ContactInformationCoding { + @Test func encodesToString() async throws { + func encode(_ contactInformation: VAPID.Configuration.ContactInformation) throws -> String { + String(decoding: try JSONEncoder().encode(contactInformation), as: UTF8.self) + } + #expect(try encode(.email("test@example.com")) == "\"mailto:test@example.com\"") + #expect(try encode(.email("junk")) == "\"mailto:junk\"") + #expect(try encode(.email("")) == "\"mailto:\"") + #expect(try encode(.url(URL(string: "https://example.com")!)) == "\"https:\\/\\/example.com\"") + #expect(try encode(.url(URL(string: "junk")!)) == "\"junk\"") + } + + @Test func decodesFromString() async throws { + func decode(_ string: String) throws -> VAPID.Configuration.ContactInformation { + try JSONDecoder().decode(VAPID.Configuration.ContactInformation.self, from: Data(string.utf8)) + } + #expect(try decode("\"mailto:test@example.com\"") == .email("test@example.com")) + #expect(try decode("\"mailto:junk\"") == .email("junk")) + #expect(try decode("\"https://example.com\"") == .url(URL(string: "https://example.com")!)) + #expect(try decode("\"HTTP://example.com\"") == .url(URL(string: "HTTP://example.com")!)) + + #expect(throws: DecodingError.self) { + try decode("\"\"") + } + + #expect(throws: DecodingError.self) { + try decode("\"junk\"") + } + + #expect(throws: DecodingError.self) { + try decode("\"file:///Users/you/Library\"") + } + + #expect(throws: DecodingError.self) { + try decode("\"mailto:\"") + } + } +} diff --git a/Tests/WebPushTests/VAPIDTokenTests.swift b/Tests/WebPushTests/VAPIDTokenTests.swift index 2b97df6..08a95c7 100644 --- a/Tests/WebPushTests/VAPIDTokenTests.swift +++ b/Tests/WebPushTests/VAPIDTokenTests.swift @@ -21,6 +21,17 @@ struct MockVAPIDKey: VAPIDKeyProtocol { } @Suite struct VAPIDTokenTests { + @Test func expirationProperlyConfigured() throws { + let date = Date(timeIntervalSince1970: 1_234_567) + let token = VAPID.Token( + origin: "https://push.example.net", + contactInformation: .email("push@example.com"), + expiration: date + ) + + #expect(token.expiration == 1_234_567) + } + @Test func generatesValidSignedToken() throws { let key = VAPID.Key() @@ -66,4 +77,24 @@ struct MockVAPIDKey: VAPIDKeyProtocol { let generatedHeader = try expectedToken.generateAuthorization(signedBy: mockKey) #expect(generatedHeader == "vapid t=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3B1c2guZXhhbXBsZS5uZXQiLCJleHAiOjE0NTM1MjM3NjgsInN1YiI6Im1haWx0bzpwdXNoQGV4YW1wbGUuY29tIn0.i3CYb7t4xfxCDquptFOepC9GAu_HLGkMlMuCGSK2rpiUfnK9ojFwDXb1JrErtmysazNjjvW2L9OkSSHzvoD1oA, k=BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs") } + + @Test func invalidTokenInitialization() { + let invalidToken = VAPID.Token(token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3B1c2guZXhhbXBsZS5uZXQiLCJleHAiOjE0NTM1MjM3NjgsInN1YiI6Im1haWx0bzpwdXNoQGV4YW1wbGUuY29tIn0.i3CYb7t4xfxCDquptFOepC9GAu_HLGkMlMuCGSK2rpiUfnK9ojFwDXb1JrErtmysazNjjvW2L9OkSSHzvoD1oA", key: "") + #expect(invalidToken == nil) + + let incompleteToken = VAPID.Token(token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3B1c2guZXhhbXBs", key: "BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs") + #expect(incompleteToken == nil) + + let invalidTokenHeader = VAPID.Token(token: "eyJ0eXAiOiJKV1QiL CJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3B1c2guZXhhbXBsZS5uZXQiLCJleHAiOjE0NTM1MjM3NjgsInN1YiI6Im1haWx0bzpwdXNoQGV4YW1wbGUuY29tIn0.i3CYb7t4xfxCDquptFOepC9GAu_HLGkMlMuCGSK2rpiUfnK9ojFwDXb1JrErtmysazNjjvW2L9OkSSHzvoD1oA", key: "BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs") + #expect(invalidTokenHeader == nil) + + let invalidTokenBody = VAPID.Token(token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3B1c2guZXhhbXBsZS5uZXQiLCJleHA iOjE0NTM1MjM3NjgsInN1YiI6Im1haWx0bzpwdXNoQGV4YW1wbGUuY29tIn0.i3CYb7t4xfxCDquptFOepC9GAu_HLGkMlMuCGSK2rpiUfnK9ojFwDXb1JrErtmysazNjjvW2L9OkSSHzvoD1oA", key: "BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs") + #expect(invalidTokenBody == nil) + + let invalidTokenSignature = VAPID.Token(token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3B1c2guZXhhbXBsZS5uZXQiLCJleHAiOjE0NTM1MjM3NjgsInN1YiI6Im1haWx0bzpwdXNoQGV4YW1wbGUuY29tIn0.i3CYb7t4xfxCDquptFOepC9GAu_HLGkMlMuCGSK2rpiUfnK 9ojFwDXb1JrErtmysazNjjvW2L9OkSSHzvoD1oA", key: "BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs") + #expect(invalidTokenSignature == nil) + + let invalidTokenKey = VAPID.Token(token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3B1c2guZXhhbXBsZS5uZXQiLCJleHAiOjE0NTM1MjM3NjgsInN1YiI6Im1haWx0bzpwdXNoQGV4YW1wbGUuY29tIn0.i3CYb7t4xfxCDquptFOepC9GAu_HLGkMlMuCGSK2rpiUfnK9ojFwDXb1JrErtmysazNjjvW2L9OkSSHzvoD1oA", key: "BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6 YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs") + #expect(invalidTokenKey == nil) + } }