1
1
//
2
- // WebPushTests .swift
2
+ // WebPushManagerTests .swift
3
3
// swift-webpush
4
4
//
5
5
// Created by Dimitri Bouniol on 2024-12-03.
@@ -45,6 +45,68 @@ struct WebPushManagerTests {
45
45
46
46
@Suite ( " Sending Messages " )
47
47
struct SendingMessages {
48
+ func validateAuthotizationHeader(
49
+ request: HTTPClientRequest ,
50
+ vapidConfiguration: VAPID . Configuration ,
51
+ origin: String
52
+ ) throws {
53
+ let auth = try #require( request. headers [ " Authorization " ] . first)
54
+ let components = auth. split ( separator: " , " )
55
+ let tComponents = try #require( components. first) . split ( separator: " = " )
56
+ let kComponents = try #require( components. last) . split ( separator: " = " )
57
+ let t = String ( try #require( tComponents. last) . trimming ( while: \. isWhitespace) )
58
+ let k = String ( try #require( kComponents. last) . trimming ( while: \. isWhitespace) )
59
+ #expect( k == vapidConfiguration. primaryKey? . id. description)
60
+
61
+ let decodedToken = try #require( VAPID . Token ( token: t, key: k) )
62
+ #expect( decodedToken. audience == origin)
63
+ #expect( decodedToken. subject == vapidConfiguration. contactInformation)
64
+ #expect( decodedToken. expiration > Int ( Date ( ) . timeIntervalSince1970) )
65
+ }
66
+
67
+ func decrypt(
68
+ request: HTTPClientRequest ,
69
+ userAgentPrivateKey: P256 . KeyAgreement . PrivateKey ,
70
+ userAgentKeyMaterial: UserAgentKeyMaterial
71
+ ) async throws -> [ UInt8 ] {
72
+ var body = try #require( try await request. body? . collect ( upTo: 16 * 1024 ) )
73
+ #expect( body. readableBytes == 4096 )
74
+
75
+ let salt = body. readBytes ( length: 16 )
76
+ let recordSize = body. readInteger ( as: UInt32 . self)
77
+ #expect( try #require( recordSize) == 4010 )
78
+ let keyIDSize = body. readInteger ( as: UInt8 . self)
79
+ let keyID = body. readBytes ( length: Int ( keyIDSize ?? 0 ) )
80
+
81
+ let userAgent = userAgentKeyMaterial
82
+ let applicationServerECDHPublicKey = try P256 . KeyAgreement. PublicKey ( x963Representation: try #require( keyID) )
83
+
84
+ let sharedSecret = try userAgentPrivateKey. sharedSecretFromKeyAgreement ( with: applicationServerECDHPublicKey)
85
+
86
+ let keyInfo = " WebPush: info " . utf8Bytes + [ 0x00 ] + userAgent. publicKey. x963Representation + applicationServerECDHPublicKey. x963Representation
87
+ let inputKeyMaterial = sharedSecret. hkdfDerivedSymmetricKey (
88
+ using: SHA256 . self,
89
+ salt: userAgent. authenticationSecret,
90
+ sharedInfo: keyInfo,
91
+ outputByteCount: 32
92
+ )
93
+
94
+ let contentEncryptionKeyInfo = " Content-Encoding: aes128gcm " . utf8Bytes + [ 0x00 ]
95
+ let contentEncryptionKey = HKDF< SHA256> . deriveKey( inputKeyMaterial: inputKeyMaterial, salt: try #require( salt) , info: contentEncryptionKeyInfo, outputByteCount: 16 )
96
+
97
+ let nonceInfo = " Content-Encoding: nonce " . utf8Bytes + [ 0x00 ]
98
+ let nonce = try HKDF < SHA256 > . deriveKey ( inputKeyMaterial: inputKeyMaterial, salt: try #require( salt) , info: nonceInfo, outputByteCount: 12 )
99
+ . withUnsafeBytes ( AES . GCM. Nonce. init ( data: ) )
100
+
101
+ let cypherText = body. readBytes ( length: body. readableBytes - 16 )
102
+ let tag = body. readBytes ( length: 16 )
103
+ let encryptedRecord = try AES . GCM. SealedBox ( nonce: nonce, ciphertext: #require( cypherText) , tag: #require( tag) )
104
+
105
+ let paddedPayload = try AES . GCM. open ( encryptedRecord, using: contentEncryptionKey)
106
+
107
+ return paddedPayload. trimmingSuffix { $0 == 0 } . dropLast ( )
108
+ }
109
+
48
110
@Test func sendSuccessfulTextMessage( ) async throws {
49
111
try await confirmation { requestWasMade in
50
112
let vapidConfiguration = VAPID . Configuration. makeTesting ( )
@@ -63,6 +125,26 @@ struct WebPushManagerTests {
63
125
vapidConfiguration: vapidConfiguration,
64
126
logger: Logger ( label: " WebPushManagerTests " , factory: { PrintLogHandler ( label: $0, metadataProvider: $1) } ) ,
65
127
executor: . httpClient( MockHTTPClient ( { request in
128
+ try validateAuthotizationHeader (
129
+ request: request,
130
+ vapidConfiguration: vapidConfiguration,
131
+ origin: " https://example.com "
132
+ )
133
+ #expect( request. method == . POST)
134
+ #expect( request. headers [ " Content-Encoding " ] == [ " aes128gcm " ] )
135
+ #expect( request. headers [ " Content-Type " ] == [ " application/octet-stream " ] )
136
+ #expect( request. headers [ " TTL " ] == [ " 2592000 " ] )
137
+ #expect( request. headers [ " Urgency " ] == [ " high " ] )
138
+ #expect( request. headers [ " Topic " ] == [ ] ) // TODO: Update when topic is added
139
+
140
+ let message = try await decrypt (
141
+ request: request,
142
+ userAgentPrivateKey: subscriberPrivateKey,
143
+ userAgentKeyMaterial: subscriber. userAgentKeyMaterial
144
+ )
145
+
146
+ #expect( String ( decoding: message, as: UTF8 . self) == " hello " )
147
+
66
148
requestWasMade ( )
67
149
return HTTPClientResponse ( status: . created)
68
150
} ) )
@@ -71,6 +153,187 @@ struct WebPushManagerTests {
71
153
try await manager. send ( string: " hello " , to: subscriber)
72
154
}
73
155
}
156
+
157
+ @Test func sendSuccessfulDataMessage( ) async throws {
158
+ try await confirmation { requestWasMade in
159
+ let vapidConfiguration = VAPID . Configuration. makeTesting ( )
160
+
161
+ let subscriberPrivateKey = P256 . KeyAgreement. PrivateKey ( compactRepresentable: false )
162
+ var authenticationSecret : [ UInt8 ] = Array ( repeating: 0 , count: 16 )
163
+ for index in authenticationSecret. indices { authenticationSecret [ index] = . random( in: . min ... . max) }
164
+
165
+ let subscriber = Subscriber (
166
+ endpoint: URL ( string: " https://example.com/subscriber " ) !,
167
+ userAgentKeyMaterial: UserAgentKeyMaterial ( publicKey: subscriberPrivateKey. publicKey, authenticationSecret: Data ( authenticationSecret) ) ,
168
+ vapidKeyID: vapidConfiguration. primaryKey!. id
169
+ )
170
+
171
+ let manager = WebPushManager (
172
+ vapidConfiguration: vapidConfiguration,
173
+ logger: Logger ( label: " WebPushManagerTests " , factory: { PrintLogHandler ( label: $0, metadataProvider: $1) } ) ,
174
+ executor: . httpClient( MockHTTPClient ( { request in
175
+ try validateAuthotizationHeader (
176
+ request: request,
177
+ vapidConfiguration: vapidConfiguration,
178
+ origin: " https://example.com "
179
+ )
180
+ #expect( request. method == . POST)
181
+ #expect( request. headers [ " Content-Encoding " ] == [ " aes128gcm " ] )
182
+ #expect( request. headers [ " Content-Type " ] == [ " application/octet-stream " ] )
183
+ #expect( request. headers [ " TTL " ] == [ " 2592000 " ] )
184
+ #expect( request. headers [ " Urgency " ] == [ " high " ] )
185
+ #expect( request. headers [ " Topic " ] == [ ] ) // TODO: Update when topic is added
186
+
187
+ let message = try await decrypt (
188
+ request: request,
189
+ userAgentPrivateKey: subscriberPrivateKey,
190
+ userAgentKeyMaterial: subscriber. userAgentKeyMaterial
191
+ )
192
+
193
+ #expect( String ( decoding: message, as: UTF8 . self) == " hello " )
194
+
195
+ requestWasMade ( )
196
+ return HTTPClientResponse ( status: . created)
197
+ } ) )
198
+ )
199
+
200
+ try await manager. send ( data: " hello " . utf8Bytes, to: subscriber)
201
+ }
202
+ }
203
+
204
+ @Test func sendSuccessfulJSONMessage( ) async throws {
205
+ try await confirmation { requestWasMade in
206
+ let vapidConfiguration = VAPID . Configuration. makeTesting ( )
207
+
208
+ let subscriberPrivateKey = P256 . KeyAgreement. PrivateKey ( compactRepresentable: false )
209
+ var authenticationSecret : [ UInt8 ] = Array ( repeating: 0 , count: 16 )
210
+ for index in authenticationSecret. indices { authenticationSecret [ index] = . random( in: . min ... . max) }
211
+
212
+ let subscriber = Subscriber (
213
+ endpoint: URL ( string: " https://example.com/subscriber " ) !,
214
+ userAgentKeyMaterial: UserAgentKeyMaterial ( publicKey: subscriberPrivateKey. publicKey, authenticationSecret: Data ( authenticationSecret) ) ,
215
+ vapidKeyID: vapidConfiguration. primaryKey!. id
216
+ )
217
+
218
+ let manager = WebPushManager (
219
+ vapidConfiguration: vapidConfiguration,
220
+ logger: Logger ( label: " WebPushManagerTests " , factory: { PrintLogHandler ( label: $0, metadataProvider: $1) } ) ,
221
+ executor: . httpClient( MockHTTPClient ( { request in
222
+ try validateAuthotizationHeader (
223
+ request: request,
224
+ vapidConfiguration: vapidConfiguration,
225
+ origin: " https://example.com "
226
+ )
227
+ #expect( request. method == . POST)
228
+ #expect( request. headers [ " Content-Encoding " ] == [ " aes128gcm " ] )
229
+ #expect( request. headers [ " Content-Type " ] == [ " application/octet-stream " ] )
230
+ #expect( request. headers [ " TTL " ] == [ " 2592000 " ] )
231
+ #expect( request. headers [ " Urgency " ] == [ " high " ] )
232
+ #expect( request. headers [ " Topic " ] == [ ] ) // TODO: Update when topic is added
233
+
234
+ let message = try await decrypt (
235
+ request: request,
236
+ userAgentPrivateKey: subscriberPrivateKey,
237
+ userAgentKeyMaterial: subscriber. userAgentKeyMaterial
238
+ )
239
+
240
+ #expect( String ( decoding: message, as: UTF8 . self) == #"{"hello":"world"}"# )
241
+
242
+ requestWasMade ( )
243
+ return HTTPClientResponse ( status: . created)
244
+ } ) )
245
+ )
246
+
247
+ try await manager. send ( json: [ " hello " : " world " ] , to: subscriber)
248
+ }
249
+ }
250
+
251
+ @Test func sendMessageToNotFoundPushServerError( ) async throws {
252
+ await confirmation { requestWasMade in
253
+ let vapidConfiguration = VAPID . Configuration. makeTesting ( )
254
+
255
+ let subscriberPrivateKey = P256 . KeyAgreement. PrivateKey ( compactRepresentable: false )
256
+ var authenticationSecret : [ UInt8 ] = Array ( repeating: 0 , count: 16 )
257
+ for index in authenticationSecret. indices { authenticationSecret [ index] = . random( in: . min ... . max) }
258
+
259
+ let subscriber = Subscriber (
260
+ endpoint: URL ( string: " https://example.com/subscriber " ) !,
261
+ userAgentKeyMaterial: UserAgentKeyMaterial ( publicKey: subscriberPrivateKey. publicKey, authenticationSecret: Data ( authenticationSecret) ) ,
262
+ vapidKeyID: vapidConfiguration. primaryKey!. id
263
+ )
264
+
265
+ let manager = WebPushManager (
266
+ vapidConfiguration: vapidConfiguration,
267
+ logger: Logger ( label: " WebPushManagerTests " , factory: { PrintLogHandler ( label: $0, metadataProvider: $1) } ) ,
268
+ executor: . httpClient( MockHTTPClient ( { request in
269
+ requestWasMade ( )
270
+ return HTTPClientResponse ( status: . notFound)
271
+ } ) )
272
+ )
273
+
274
+ await #expect( throws: BadSubscriberError ( ) ) {
275
+ try await manager. send ( string: " hello " , to: subscriber)
276
+ }
277
+ }
278
+ }
279
+
280
+ @Test func sendMessageToGonePushServerError( ) async throws {
281
+ await confirmation { requestWasMade in
282
+ let vapidConfiguration = VAPID . Configuration. makeTesting ( )
283
+
284
+ let subscriberPrivateKey = P256 . KeyAgreement. PrivateKey ( compactRepresentable: false )
285
+ var authenticationSecret : [ UInt8 ] = Array ( repeating: 0 , count: 16 )
286
+ for index in authenticationSecret. indices { authenticationSecret [ index] = . random( in: . min ... . max) }
287
+
288
+ let subscriber = Subscriber (
289
+ endpoint: URL ( string: " https://example.com/subscriber " ) !,
290
+ userAgentKeyMaterial: UserAgentKeyMaterial ( publicKey: subscriberPrivateKey. publicKey, authenticationSecret: Data ( authenticationSecret) ) ,
291
+ vapidKeyID: vapidConfiguration. primaryKey!. id
292
+ )
293
+
294
+ let manager = WebPushManager (
295
+ vapidConfiguration: vapidConfiguration,
296
+ logger: Logger ( label: " WebPushManagerTests " , factory: { PrintLogHandler ( label: $0, metadataProvider: $1) } ) ,
297
+ executor: . httpClient( MockHTTPClient ( { request in
298
+ requestWasMade ( )
299
+ return HTTPClientResponse ( status: . gone)
300
+ } ) )
301
+ )
302
+
303
+ await #expect( throws: BadSubscriberError ( ) ) {
304
+ try await manager. send ( string: " hello " , to: subscriber)
305
+ }
306
+ }
307
+ }
308
+
309
+ @Test func sendMessageToUnknownPushServerError( ) async throws {
310
+ await confirmation { requestWasMade in
311
+ let vapidConfiguration = VAPID . Configuration. makeTesting ( )
312
+
313
+ let subscriberPrivateKey = P256 . KeyAgreement. PrivateKey ( compactRepresentable: false )
314
+ var authenticationSecret : [ UInt8 ] = Array ( repeating: 0 , count: 16 )
315
+ for index in authenticationSecret. indices { authenticationSecret [ index] = . random( in: . min ... . max) }
316
+
317
+ let subscriber = Subscriber (
318
+ endpoint: URL ( string: " https://example.com/subscriber " ) !,
319
+ userAgentKeyMaterial: UserAgentKeyMaterial ( publicKey: subscriberPrivateKey. publicKey, authenticationSecret: Data ( authenticationSecret) ) ,
320
+ vapidKeyID: vapidConfiguration. primaryKey!. id
321
+ )
322
+
323
+ let manager = WebPushManager (
324
+ vapidConfiguration: vapidConfiguration,
325
+ logger: Logger ( label: " WebPushManagerTests " , factory: { PrintLogHandler ( label: $0, metadataProvider: $1) } ) ,
326
+ executor: . httpClient( MockHTTPClient ( { request in
327
+ requestWasMade ( )
328
+ return HTTPClientResponse ( status: . internalServerError)
329
+ } ) )
330
+ )
331
+
332
+ await #expect( throws: HTTPError . self) {
333
+ try await manager. send ( string: " hello " , to: subscriber)
334
+ }
335
+ }
336
+ }
74
337
}
75
338
76
339
@Suite
0 commit comments