<lambda>null1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
2 * Any copyright is dedicated to the Public Domain.
3 http://creativecommons.org/publicdomain/zero/1.0/ */
4
5 package org.mozilla.geckoview.test
6
7 import android.os.Parcel
8 import androidx.test.filters.MediumTest
9 import androidx.test.ext.junit.runners.AndroidJUnit4
10 import android.util.Base64
11 import org.hamcrest.MatcherAssert.assertThat
12 import org.hamcrest.Matchers.*
13 import org.json.JSONObject
14 import org.junit.After
15 import org.junit.Before
16 import org.junit.Test
17 import org.junit.runner.RunWith
18 import org.mozilla.geckoview.*
19 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
20 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.RejectedPromiseException
21 import org.mozilla.geckoview.test.util.Callbacks
22 import java.security.KeyPair
23 import java.security.KeyPairGenerator
24 import java.security.SecureRandom
25 import java.security.interfaces.ECPublicKey
26 import java.security.spec.ECGenParameterSpec
27
28 @RunWith(AndroidJUnit4::class)
29 @MediumTest
30 class WebPushTest : BaseSessionTest() {
31 companion object {
32 val PUSH_ENDPOINT: String = "https://test.endpoint"
33 val APP_SERVER_KEY_PAIR: KeyPair = generateKeyPair()
34 val AUTH_SECRET: ByteArray = generateAuthSecret()
35 val BROWSER_KEY_PAIR: KeyPair = generateKeyPair()
36
37 private fun generateKeyPair(): KeyPair {
38 try {
39 val spec = ECGenParameterSpec("secp256r1")
40 val generator = KeyPairGenerator.getInstance("EC")
41 generator.initialize(spec)
42 return generator.generateKeyPair()
43 } catch (e: Exception) {
44 throw RuntimeException(e)
45 }
46 }
47
48 private fun generateAuthSecret(): ByteArray {
49 val bytes = ByteArray(16)
50 SecureRandom().nextBytes(bytes)
51
52 return bytes
53 }
54 }
55
56 var delegate: TestPushDelegate? = null
57
58 @Before
59 fun setup() {
60 sessionRule.setPrefsUntilTestEnd(mapOf("dom.webnotifications.requireuserinteraction" to false))
61 // Grant "desktop notification" permission
62 mainSession.delegateUntilTestEnd(object : Callbacks.PermissionDelegate {
63 override fun onContentPermissionRequest(session: GeckoSession, perm: GeckoSession.PermissionDelegate.ContentPermission):
64 GeckoResult<Int>? {
65 assertThat("Should grant DESKTOP_NOTIFICATIONS permission", perm.permission, equalTo(GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION))
66 return GeckoResult.fromValue(GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW)
67 }
68 })
69
70 delegate = TestPushDelegate()
71
72 sessionRule.addExternalDelegateUntilTestEnd(WebPushDelegate::class,
73 { d -> sessionRule.runtime.webPushController.setDelegate(d) },
74 { sessionRule.runtime.webPushController.setDelegate(null) }, delegate!!)
75
76
77 mainSession.loadTestPath(PUSH_HTML_PATH)
78 mainSession.waitForPageStop()
79 }
80
81 @After
82 fun tearDown() {
83 sessionRule.runtime.webPushController.setDelegate(null)
84 delegate = null
85 }
86
87 private fun verifySubscription(subscription: JSONObject) {
88 assertThat("Push endpoint should match", subscription.getString("endpoint"), equalTo(PUSH_ENDPOINT))
89
90 val keys = subscription.getJSONObject("keys")
91 val authSecret = Base64.decode(keys.getString("auth"), Base64.URL_SAFE)
92 val encryptionKey = WebPushUtils.keyFromString(keys.getString("p256dh"))
93
94 assertThat("Auth secret should match", authSecret, equalTo(AUTH_SECRET))
95 assertThat("Encryption key should match", encryptionKey, equalTo(BROWSER_KEY_PAIR.public))
96 }
97
98 @Test
99 fun subscribe() {
100 // PushManager.subscribe()
101 val appServerKey = WebPushUtils.keyToString(APP_SERVER_KEY_PAIR.public as ECPublicKey)
102 var pushSubscription = mainSession.evaluatePromiseJS("window.doSubscribe(\"$appServerKey\")").value as JSONObject
103 assertThat("Should have a stored subscription", delegate!!.storedSubscription, notNullValue())
104 verifySubscription(pushSubscription)
105
106 // PushManager.getSubscription()
107 pushSubscription = mainSession.evaluatePromiseJS("window.doGetSubscription()").value as JSONObject
108 verifySubscription(pushSubscription)
109 }
110
111 @Test
112 fun subscribeNoAppServerKey() {
113 // PushManager.subscribe()
114 var pushSubscription = mainSession.evaluatePromiseJS("window.doSubscribe()").value as JSONObject
115 assertThat("Should have a stored subscription", delegate!!.storedSubscription, notNullValue())
116 verifySubscription(pushSubscription)
117
118 // PushManager.getSubscription()
119 pushSubscription = mainSession.evaluatePromiseJS("window.doGetSubscription()").value as JSONObject
120 verifySubscription(pushSubscription)
121 }
122
123 @Test(expected = RejectedPromiseException::class)
124 fun subscribeNullDelegate() {
125 sessionRule.runtime.webPushController.setDelegate(null)
126 mainSession.evaluatePromiseJS("window.doSubscribe()").value as JSONObject
127 }
128
129 @Test(expected = RejectedPromiseException::class)
130 fun getSubscriptionNullDelegate() {
131 sessionRule.runtime.webPushController.setDelegate(null)
132 mainSession.evaluatePromiseJS("window.doGetSubscription()").value as JSONObject
133 }
134
135 @Test
136 fun unsubscribe() {
137 subscribe()
138
139 // PushManager.unsubscribe()
140 val unsubResult = mainSession.evaluatePromiseJS("window.doUnsubscribe()").value as JSONObject
141 assertThat("Unsubscribe result should be non-null", unsubResult, notNullValue())
142 assertThat("Should not have a stored subscription", delegate!!.storedSubscription, nullValue())
143 }
144
145 @Test
146 fun pushEvent() {
147 subscribe()
148
149 val p = mainSession.evaluatePromiseJS("window.doWaitForPushEvent()")
150
151 val testPayload = "The Payload";
152 sessionRule.runtime.webPushController.onPushEvent(delegate!!.storedSubscription!!.scope, testPayload.toByteArray(Charsets.UTF_8))
153
154 assertThat("Push data should match", p.value as String, equalTo(testPayload))
155 }
156
157 @Test
158 fun pushEventWithoutData() {
159 subscribe()
160
161 val p = mainSession.evaluatePromiseJS("window.doWaitForPushEvent()")
162
163 sessionRule.runtime.webPushController.onPushEvent(delegate!!.storedSubscription!!.scope, null)
164
165 assertThat("Push data should be empty", p.value as String, equalTo(""))
166 }
167
168 private fun sendNotification() {
169 val notificationResult = GeckoResult<Void>()
170 val runtime = sessionRule.runtime
171 val register = { delegate: WebNotificationDelegate -> runtime.webNotificationDelegate = delegate}
172 val unregister = { _: WebNotificationDelegate -> runtime.webNotificationDelegate = null }
173
174 val expectedTitle = "The title"
175 val expectedBody = "The body"
176
177 sessionRule.addExternalDelegateDuringNextWait(WebNotificationDelegate::class, register,
178 unregister, object : WebNotificationDelegate {
179 @GeckoSessionTestRule.AssertCalled
180 override fun onShowNotification(notification: WebNotification) {
181 assertThat("Title should match", notification.title, equalTo(expectedTitle))
182 assertThat("Body should match", notification.text, equalTo(expectedBody))
183 assertThat("Source should match", notification.source, endsWith("sw.js"))
184 notificationResult.complete(null)
185 }
186 })
187
188 val testPayload = JSONObject()
189 testPayload.put("title", expectedTitle)
190 testPayload.put("body", expectedBody)
191
192 sessionRule.runtime.webPushController.onPushEvent(delegate!!.storedSubscription!!.scope, testPayload.toString().toByteArray(Charsets.UTF_8))
193 sessionRule.waitForResult(notificationResult)
194 }
195
196 @Test
197 fun pushEventWithNotification() {
198 subscribe()
199 sendNotification()
200 }
201
202 @Test
203 fun subscriptionChanged() {
204 subscribe()
205
206 val p = mainSession.evaluatePromiseJS("window.doWaitForSubscriptionChange()")
207
208 sessionRule.runtime.webPushController.onSubscriptionChanged(delegate!!.storedSubscription!!.scope)
209
210 assertThat("Result should not be null", p.value, notNullValue())
211 }
212
213 @Test(expected = IllegalArgumentException::class)
214 fun invalidDuplicateKeys() {
215 WebPushSubscription("https://scope", PUSH_ENDPOINT,
216 WebPushUtils.keyToBytes(APP_SERVER_KEY_PAIR.public as ECPublicKey),
217 WebPushUtils.keyToBytes(APP_SERVER_KEY_PAIR.public as ECPublicKey)!!, AUTH_SECRET)
218 }
219
220 @Test
221 fun parceling() {
222 val testScope = "https://test.scope";
223 val sub = WebPushSubscription(testScope, PUSH_ENDPOINT,
224 WebPushUtils.keyToBytes(APP_SERVER_KEY_PAIR.public as ECPublicKey),
225 WebPushUtils.keyToBytes(BROWSER_KEY_PAIR.public as ECPublicKey)!!, AUTH_SECRET)
226
227 val parcel = Parcel.obtain()
228 sub.writeToParcel(parcel, 0)
229 parcel.setDataPosition(0)
230
231 val sub2 = WebPushSubscription.CREATOR.createFromParcel(parcel)
232 assertThat("Scope should match", sub.scope, equalTo(sub2.scope))
233 assertThat("Endpoint should match", sub.endpoint, equalTo(sub2.endpoint))
234 assertThat("App server key should match", sub.appServerKey, equalTo(sub2.appServerKey))
235 assertThat("Encryption key should match", sub.browserPublicKey, equalTo(sub2.browserPublicKey))
236 assertThat("Auth secret should match", sub.authSecret, equalTo(sub2.authSecret))
237 }
238
239 class TestPushDelegate : WebPushDelegate {
240 var storedSubscription: WebPushSubscription? = null
241
242 override fun onGetSubscription(scope: String): GeckoResult<WebPushSubscription>? {
243 return GeckoResult.fromValue(storedSubscription)
244 }
245
246 override fun onUnsubscribe(scope: String): GeckoResult<Void>? {
247 storedSubscription = null
248 return GeckoResult.fromValue(null)
249 }
250
251 override fun onSubscribe(scope: String, appServerKey: ByteArray?): GeckoResult<WebPushSubscription>? {
252 appServerKey?.let { assertThat("Application server key should match", it, equalTo(WebPushUtils.keyToBytes(APP_SERVER_KEY_PAIR.public as ECPublicKey))) }
253 storedSubscription = WebPushSubscription(scope, PUSH_ENDPOINT, appServerKey, WebPushUtils.keyToBytes(BROWSER_KEY_PAIR.public as ECPublicKey)!!, AUTH_SECRET)
254 return GeckoResult.fromValue(storedSubscription)
255 }
256 }
257 }
258