Skip to content

Commit 5c342a4

Browse files
Added tests for declarative push notifications
Fixes #69
1 parent 7485d11 commit 5c342a4

File tree

3 files changed

+294
-2
lines changed

3 files changed

+294
-2
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// NeverTests.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2025-03-01.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
10+
#if canImport(FoundationEssentials)
11+
import FoundationEssentials
12+
#else
13+
import Foundation
14+
#endif
15+
import Testing
16+
@testable import WebPush
17+
18+
@Suite("Never Tests")
19+
struct NeverTests {
20+
@Test func retroactiveCodableWorks() async throws {
21+
#expect(throws: DecodingError.self, performing: {
22+
try JSONDecoder().decode(Never.self, from: Data("null".utf8))
23+
})
24+
}
25+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//
2+
// NotificationTests.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2025-03-01.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
10+
#if canImport(FoundationEssentials)
11+
import FoundationEssentials
12+
#else
13+
import Foundation
14+
#endif
15+
import Testing
16+
@testable import WebPush
17+
18+
@Suite("Push Message Notification")
19+
struct NotificationTests {
20+
@Test func simpleNotificationEncodesProperly() async throws {
21+
let notification = PushMessage.Notification(
22+
destination: URL(string: "https://jiiiii.moe")!,
23+
title: "New Anime",
24+
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
25+
)
26+
27+
let encoder = JSONEncoder()
28+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
29+
30+
let encodedString = String(decoding: try encoder.encode(notification), as: UTF8.self)
31+
#expect(encodedString == """
32+
{
33+
"notification" : {
34+
"navigate" : "https://jiiiii.moe",
35+
"timestamp" : 1000000000000,
36+
"title" : "New Anime"
37+
},
38+
"web_push" : 8030
39+
}
40+
""")
41+
42+
let decodedNotification = try JSONDecoder().decode(PushMessage.SimpleNotification.self, from: Data(encodedString.utf8))
43+
#expect(decodedNotification == notification)
44+
}
45+
46+
@Test func legacyNotificationEncodesProperly() async throws {
47+
let notification = PushMessage.Notification(
48+
kind: .legacy,
49+
destination: URL(string: "https://jiiiii.moe")!,
50+
title: "New Anime",
51+
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
52+
)
53+
54+
let encoder = JSONEncoder()
55+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
56+
57+
let encodedString = String(decoding: try encoder.encode(notification), as: UTF8.self)
58+
#expect(encodedString == """
59+
{
60+
"notification" : {
61+
"navigate" : "https://jiiiii.moe",
62+
"timestamp" : 1000000000000,
63+
"title" : "New Anime"
64+
}
65+
}
66+
""")
67+
68+
let decodedNotification = try JSONDecoder().decode(PushMessage.SimpleNotification.self, from: Data(encodedString.utf8))
69+
#expect(decodedNotification == notification)
70+
}
71+
72+
@Test func completeNotificationEncodesProperly() async throws {
73+
let notification = PushMessage.Notification(
74+
kind: .declarative,
75+
destination: URL(string: "https://jiiiii.moe")!,
76+
title: "New Anime",
77+
body: "New anime is available!",
78+
image: URL(string: "https://jiiiii.moe/animeImage")!,
79+
actions: [
80+
PushMessage.NotificationAction(
81+
id: "ok",
82+
label: "OK",
83+
destination: URL(string: "https://jiiiii.moe/ok")!,
84+
icon: URL(string: "https://jiiiii.moe/okIcon")
85+
),
86+
PushMessage.NotificationAction(
87+
id: "cancel",
88+
label: "Cancel",
89+
destination: URL(string: "https://jiiiii.moe/cancel")!,
90+
icon: URL(string: "https://jiiiii.moe/cancelIcon")
91+
),
92+
],
93+
timestamp: Date(timeIntervalSince1970: 1_000_000_000),
94+
appBadgeCount: 0,
95+
isMutable: true,
96+
options: PushMessage.NotificationOptions(
97+
direction: .rightToLeft,
98+
language: "jp",
99+
tag: "new-anime",
100+
icon: URL(string: "https://jiiiii.moe/icon")!,
101+
badgeIcon: URL(string: "https://jiiiii.moe/badgeIcon")!,
102+
vibrate: [200, 100, 200],
103+
shouldRenotify: true,
104+
isSilent: true,
105+
requiresInteraction: true
106+
)
107+
)
108+
109+
let encoder = JSONEncoder()
110+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
111+
112+
let encodedString = String(decoding: try encoder.encode(notification), as: UTF8.self)
113+
#expect(encodedString == """
114+
{
115+
"app_badge" : 0,
116+
"mutable" : true,
117+
"notification" : {
118+
"actions" : [
119+
{
120+
"action" : "ok",
121+
"icon" : "https://jiiiii.moe/okIcon",
122+
"navigate" : "https://jiiiii.moe/ok",
123+
"title" : "OK"
124+
},
125+
{
126+
"action" : "cancel",
127+
"icon" : "https://jiiiii.moe/cancelIcon",
128+
"navigate" : "https://jiiiii.moe/cancel",
129+
"title" : "Cancel"
130+
}
131+
],
132+
"badge" : "https://jiiiii.moe/badgeIcon",
133+
"body" : "New anime is available!",
134+
"dir" : "rtf",
135+
"icon" : "https://jiiiii.moe/icon",
136+
"image" : "https://jiiiii.moe/animeImage",
137+
"lang" : "jp",
138+
"navigate" : "https://jiiiii.moe",
139+
"renotify" : true,
140+
"require_interaction" : true,
141+
"silent" : true,
142+
"tag" : "new-anime",
143+
"timestamp" : 1000000000000,
144+
"title" : "New Anime",
145+
"vibrate" : [
146+
200,
147+
100,
148+
200
149+
]
150+
},
151+
"web_push" : 8030
152+
}
153+
""")
154+
155+
let decodedNotification = try JSONDecoder().decode(PushMessage.SimpleNotification.self, from: Data(encodedString.utf8))
156+
#expect(decodedNotification == notification)
157+
}
158+
159+
@Test func customNotificationEncodesProperly() async throws {
160+
let notification = PushMessage.Notification(
161+
destination: URL(string: "https://jiiiii.moe")!,
162+
title: "New Anime",
163+
timestamp: Date(timeIntervalSince1970: 1_000_000_000),
164+
data: ["episodeID": "123"]
165+
)
166+
167+
let encoder = JSONEncoder()
168+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
169+
170+
let encodedString = String(decoding: try encoder.encode(notification), as: UTF8.self)
171+
#expect(encodedString == """
172+
{
173+
"notification" : {
174+
"data" : {
175+
"episodeID" : "123"
176+
},
177+
"navigate" : "https://jiiiii.moe",
178+
"timestamp" : 1000000000000,
179+
"title" : "New Anime"
180+
},
181+
"web_push" : 8030
182+
}
183+
""")
184+
185+
let decodedNotification = try JSONDecoder().decode(type(of: notification), from: Data(encodedString.utf8))
186+
#expect(decodedNotification == notification)
187+
}
188+
}

Tests/WebPushTests/WebPushManagerTests.swift

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,64 @@ struct WebPushManagerTests {
434434
}
435435
}
436436

437+
@Test func sendSuccessfulNotification() async throws {
438+
try await confirmation { requestWasMade in
439+
let vapidConfiguration = VAPID.Configuration.makeTesting()
440+
441+
let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
442+
var authenticationSecret: [UInt8] = Array(repeating: 0, count: 16)
443+
for index in authenticationSecret.indices { authenticationSecret[index] = .random(in: .min ... .max) }
444+
445+
let subscriber = Subscriber(
446+
endpoint: URL(string: "https://example.com/subscriber")!,
447+
userAgentKeyMaterial: UserAgentKeyMaterial(publicKey: subscriberPrivateKey.publicKey, authenticationSecret: Data(authenticationSecret)),
448+
vapidKeyID: vapidConfiguration.primaryKey!.id
449+
)
450+
451+
let notification = PushMessage.Notification(
452+
destination: URL(string: "https://jiiiii.moe")!,
453+
title: "New Anime",
454+
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
455+
)
456+
457+
let manager = WebPushManager(
458+
vapidConfiguration: vapidConfiguration,
459+
backgroundActivityLogger: Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) }),
460+
executor: .httpClient(MockHTTPClient({ request in
461+
try validateAuthotizationHeader(
462+
request: request,
463+
vapidConfiguration: vapidConfiguration,
464+
origin: "https://example.com"
465+
)
466+
#expect(request.method == .POST)
467+
#expect(request.headers["Content-Encoding"] == ["aes128gcm"])
468+
#expect(request.headers["Content-Type"] == ["application/octet-stream"])
469+
#expect(request.headers["TTL"] == ["2592000"])
470+
#expect(request.headers["Urgency"] == ["high"])
471+
#expect(request.headers["Topic"] == [])
472+
473+
let message = try await decrypt(
474+
request: request,
475+
userAgentPrivateKey: subscriberPrivateKey,
476+
userAgentKeyMaterial: subscriber.userAgentKeyMaterial
477+
)
478+
479+
let decodedNotification = try JSONDecoder().decode(PushMessage.SimpleNotification.self, from: Data(message))
480+
481+
#expect(decodedNotification == notification)
482+
483+
requestWasMade()
484+
return HTTPClientResponse(status: .created)
485+
}))
486+
)
487+
488+
try await manager.send(
489+
notification: notification,
490+
to: subscriber
491+
)
492+
}
493+
}
494+
437495
@Test func sendSuccessfulMultipleMessages() async throws {
438496
try await confirmation(expectedCount: 3) { requestWasMade in
439497
let manager = WebPushManager(
@@ -452,7 +510,7 @@ struct WebPushManagerTests {
452510
}
453511

454512
@Test func sendCustomTopic() async throws {
455-
try await confirmation(expectedCount: 6) { requestWasMade in
513+
try await confirmation(expectedCount: 8) { requestWasMade in
456514
let vapidConfiguration = VAPID.Configuration.makeTesting()
457515

458516
let subscriberPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
@@ -468,6 +526,12 @@ struct WebPushManagerTests {
468526
var logger = Logger(label: "WebPushManagerTests", factory: { PrintLogHandler(label: $0, metadataProvider: $1) })
469527
logger.logLevel = .trace
470528

529+
let notification = PushMessage.Notification(
530+
destination: URL(string: "https://jiiiii.moe")!,
531+
title: "New Anime",
532+
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
533+
)
534+
471535
let manager = WebPushManager(
472536
vapidConfiguration: vapidConfiguration,
473537
networkConfiguration: .init(alwaysResolveTopics: true),
@@ -491,7 +555,12 @@ struct WebPushManagerTests {
491555
userAgentKeyMaterial: subscriber.userAgentKeyMaterial
492556
)
493557

494-
#expect(String(decoding: message, as: UTF8.self) == "\"hello\"")
558+
if message.count == 7 {
559+
#expect(String(decoding: message, as: UTF8.self) == #""hello""#)
560+
} else {
561+
let decodedNotification = try JSONDecoder().decode(PushMessage.SimpleNotification.self, from: Data(message))
562+
#expect(decodedNotification == notification)
563+
}
495564

496565
requestWasMade()
497566
return HTTPClientResponse(status: .created)
@@ -528,6 +597,16 @@ struct WebPushManagerTests {
528597
to: subscriber,
529598
encodableDeduplicationTopic: "topic-id"
530599
)
600+
try await manager.send(
601+
notification: notification,
602+
to: subscriber,
603+
deduplicationTopic: Topic(encodableTopic: "topic-id", salt: subscriber.userAgentKeyMaterial.authenticationSecret)
604+
)
605+
try await manager.send(
606+
notification: notification,
607+
to: subscriber,
608+
encodableDeduplicationTopic: "topic-id"
609+
)
531610
}
532611
}
533612

0 commit comments

Comments
 (0)