<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