Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies {
testImplementation(libs.playwright)
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)
testImplementation(libs.kotlinx.coroutines)
}

kotlin {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ktor = "2.3.10"
slf4j = "2.0.13"
playwright = "1.43.0"
junit = "5.10.2"
kotlinx-coroutines = "1.8.0"

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Expand All @@ -17,6 +18,7 @@ git-versioning = { id = "me.qoomon.git-versioning", version.ref = "git-versionin
[libraries]
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
ktor-server-html-builder = { module = "io.ktor:ktor-server-html-builder", version.ref = "ktor" }
playwright = { module = "com.microsoft.playwright:playwright", version.ref = "playwright" }
Expand Down
83 changes: 77 additions & 6 deletions src/main/kotlin/com/interaso/webpush/WebPushService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.interaso.webpush
import java.net.*
import java.net.http.*
import java.net.http.HttpResponse.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future

/**
* Represents a service for sending web push notifications.
Expand Down Expand Up @@ -68,20 +70,89 @@ public class WebPushService(
topic: String? = null,
urgency: WebPush.Urgency? = null,
): WebPush.SubscriptionState {
val request = getRequest(payload, endpoint, p256dh, auth, ttl, topic, urgency)
val response = httpClient.send(request, BodyHandlers.ofString())
return getSubscriptionState(response)
}

/**
* Asynchronously sends a push notification using the given endpoint and credentials.
*
* @param payload The message payload to be sent in the push notification.
* @param endpoint The URL endpoint that identifies the push service subscription.
* @param p256dh The Base64-encoded P256DH key for authentication with the push service provider.
* @param auth The Base64-encoded authentication secret for the push service provider.
* @param ttl The time-to-live value for the push notification (optional).
* @param topic The topic of the push notification (optional).
* @param urgency The urgency level of the push notification (optional).
*
* @return current state of this subscription
* @throws WebPushStatusException if an unexpected status code is received from the push service.
* @throws WebPushException if an unexpected exception is caught while constructing request.
*/
public fun sendAsync(
payload: String,
endpoint: String,
p256dh: String,
auth: String,
ttl: Int? = null,
topic: String? = null,
urgency: WebPush.Urgency? = null,
): CompletableFuture<WebPush.SubscriptionState> {
return sendAsync(payload.toByteArray(), endpoint, decodeBase64(p256dh), decodeBase64(auth), ttl, topic, urgency)
}

/**
* Asynchronously sends a push notification using the given endpoint and credentials.
*
* @param payload The message payload to be sent in the push notification.
* @param endpoint The URL endpoint that identifies the push service subscription.
* @param p256dh The P256DH key for authentication with the push service provider.
* @param auth The authentication secret for the push service provider.
* @param ttl The time-to-live value for the push notification (optional).
* @param topic The topic of the push notification (optional).
* @param urgency The urgency level of the push notification (optional).
*
* @return current state of this subscription
* @throws WebPushStatusException if an unexpected status code is received from the push service.
* @throws WebPushException if an unexpected exception is caught while constructing request.
*/
public fun sendAsync(
payload: ByteArray,
endpoint: String,
p256dh: ByteArray,
auth: ByteArray,
ttl: Int? = null,
topic: String? = null,
urgency: WebPush.Urgency? = null,
): CompletableFuture<WebPush.SubscriptionState> {
val request = getRequest(payload, endpoint, p256dh, auth, ttl, topic, urgency)
val response = httpClient.sendAsync(request, BodyHandlers.ofString())
return response.thenApply { getSubscriptionState(it) }
}

private fun getRequest(
payload: ByteArray,
endpoint: String,
p256dh: ByteArray,
auth: ByteArray,
ttl: Int?,
topic: String?,
urgency: WebPush.Urgency?,
) : HttpRequest {
val body = webPush.getBody(payload, p256dh, auth)
val headers = webPush.getHeaders(endpoint, ttl, topic, urgency)

val request = HttpRequest.newBuilder()
return HttpRequest.newBuilder()
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.uri(URI.create(endpoint))
.apply { headers.forEach { setHeader(it.key, it.value) } }
.build()
}

val response = httpClient.send(request, BodyHandlers.ofString())

return webPush.getSubscriptionState(
private fun getSubscriptionState(response: HttpResponse<String>) =
webPush.getSubscriptionState(
response.statusCode(),
response.body(),
)
}
}
}
16 changes: 15 additions & 1 deletion src/test/kotlin/com/interaso/webpush/BrowserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.future.await
import kotlinx.html.*
import org.junit.jupiter.api.*
import java.nio.file.*
Expand All @@ -22,6 +23,19 @@ import kotlin.io.path.*
class BrowserTest {
@Test
fun shouldReceiveNotification() {
setupTest { webPush, notification, endpoint, p256dh, auth ->
webPush.send(notification, endpoint, p256dh, auth)
}
}

@Test
fun shouldReceiveNotificationAsync() {
setupTest { webPush, notification, endpoint, p256dh, auth ->
webPush.sendAsync(notification, endpoint, p256dh, auth).await()
}
}

private fun setupTest(send: suspend (webPush: WebPushService, notification: String, endpoint: String, p256dh: String, auth: String) -> Unit) {
val vapidKeys = VapidKeys.fromUncompressedBytes(
"BJwwFRoDoOx2vQPfvbeo-m1fZZHo6lIjtyTlWHjLNSCtHuWdGryZD5xt0LeawVQq7G60ioID1sC33fEoQT8jCzg",
"P5GjTLppISlmUyNiZqZi0HNq7GXFniAdcBECNsKBxfI",
Expand Down Expand Up @@ -51,7 +65,7 @@ class BrowserTest {
val p256dh: String by params
val auth: String by params

webPush.send(notification, endpoint, p256dh, auth)
send(webPush, notification, endpoint, p256dh, auth)
call.respondText("OK")
}
}
Expand Down