Skip to content

Commit 0860eef

Browse files
Added tests for declarative push notifications
Fixes #69
1 parent 695e1e7 commit 0860eef

File tree

3 files changed

+292
-2
lines changed

3 files changed

+292
-2
lines changed

Tests/WebPushTests/NeverTests.swift

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

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)