GeckoEngineSessionTest.kt 79.9 KB
Newer Older
1
2
3
4
5
6
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.browser.engine.gecko

7
import android.os.Handler
8
import android.os.Message
9
10
11
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
12
import mozilla.components.browser.errorpages.ErrorType
13
import mozilla.components.concept.engine.DefaultSettings
14
import mozilla.components.concept.engine.EngineSession
15
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
16
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
17
18
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.CookiePolicy
19
import mozilla.components.concept.engine.EngineSession.SafeBrowsingPolicy
20
import mozilla.components.concept.engine.HitResult
21
import mozilla.components.concept.engine.UnsupportedSettingException
22
import mozilla.components.concept.engine.content.blocking.Tracker
Grisha Kruglov's avatar
Grisha Kruglov committed
23
import mozilla.components.concept.engine.history.HistoryTrackingDelegate
24
import mozilla.components.concept.engine.manifest.WebAppManifest
25
import mozilla.components.concept.engine.permission.PermissionRequest
26
import mozilla.components.concept.engine.request.RequestInterceptor
27
import mozilla.components.concept.engine.window.WindowRequest
28
29
import mozilla.components.concept.storage.PageVisit
import mozilla.components.concept.storage.RedirectSource
30
31
import mozilla.components.concept.storage.VisitType
import mozilla.components.support.test.any
32
import mozilla.components.support.test.eq
33
import mozilla.components.support.test.expectException
34
import mozilla.components.support.test.mock
35
import mozilla.components.support.test.whenever
36
import mozilla.components.support.utils.ThreadUtils
37
import mozilla.components.test.ReflectionUtils
38
import org.json.JSONObject
39
import org.junit.Assert.assertEquals
40
import org.junit.Assert.assertFalse
41
import org.junit.Assert.assertNotNull
42
import org.junit.Assert.assertNull
43
import org.junit.Assert.assertSame
44
import org.junit.Assert.assertTrue
45
import org.junit.Before
46
47
import org.junit.Test
import org.junit.runner.RunWith
48
import org.mockito.ArgumentCaptor
49
import org.mockito.ArgumentMatchers.anyBoolean
50
import org.mockito.ArgumentMatchers.anyInt
51
import org.mockito.ArgumentMatchers.anyList
52
import org.mockito.ArgumentMatchers.anyString
53
import org.mockito.Mockito.never
54
import org.mockito.Mockito.times
55
import org.mockito.Mockito.verify
56
import org.mockito.Mockito.verifyZeroInteractions
57
import org.mozilla.geckoview.AllowOrDeny
58
import org.mozilla.geckoview.ContentBlocking
59
import org.mozilla.geckoview.ContentBlockingController
60
import org.mozilla.geckoview.GeckoResult
61
62
import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoSession
63
64
65
66
import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement.TYPE_AUDIO
import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE
import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement.TYPE_NONE
import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO
67
import org.mozilla.geckoview.GeckoSession.ProgressDelegate.SecurityInformation
68
import org.mozilla.geckoview.GeckoSessionSettings
69
import org.mozilla.geckoview.MockWebResponseInfo
70
import org.mozilla.geckoview.SessionFinder
71
import org.mozilla.geckoview.WebRequestError
72
import org.mozilla.geckoview.WebRequestError.ERROR_CATEGORY_UNKNOWN
73
import org.mozilla.geckoview.WebRequestError.ERROR_MALFORMED_URI
74
import org.mozilla.geckoview.WebRequestError.ERROR_UNKNOWN
75
76
77
typealias GeckoAntiTracking = ContentBlocking.AntiTracking
typealias GeckoSafeBrowsing = ContentBlocking.SafeBrowsing
typealias GeckoCookieBehavior = ContentBlocking.CookieBehavior
78

79
80
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
81
class GeckoEngineSessionTest {
82

83
84
85
86
87
88
89
    private lateinit var geckoSession: GeckoSession
    private lateinit var geckoSessionProvider: () -> GeckoSession

    private lateinit var navigationDelegate: ArgumentCaptor<GeckoSession.NavigationDelegate>
    private lateinit var progressDelegate: ArgumentCaptor<GeckoSession.ProgressDelegate>
    private lateinit var contentDelegate: ArgumentCaptor<GeckoSession.ContentDelegate>
    private lateinit var permissionDelegate: ArgumentCaptor<GeckoSession.PermissionDelegate>
90
    private lateinit var contentBlockingDelegate: ArgumentCaptor<ContentBlocking.Delegate>
91
92
    private lateinit var historyDelegate: ArgumentCaptor<GeckoSession.HistoryDelegate>

93
94
    @Before
    fun setup() {
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
        ThreadUtils.setHandlerForTest(object : Handler() {
            override fun sendMessageAtTime(msg: Message?, uptimeMillis: Long): Boolean {
                val wrappedRunnable = Runnable {
                    try {
                        msg?.callback?.run()
                    } catch (t: Throwable) {
                        // We ignore this in the test as the runnable could be calling
                        // a native method (disposeNative) which won't work in Robolectric
                    }
                }
                return super.sendMessageAtTime(Message.obtain(this, wrappedRunnable), uptimeMillis)
            }
        })

        navigationDelegate = ArgumentCaptor.forClass(GeckoSession.NavigationDelegate::class.java)
        progressDelegate = ArgumentCaptor.forClass(GeckoSession.ProgressDelegate::class.java)
        contentDelegate = ArgumentCaptor.forClass(GeckoSession.ContentDelegate::class.java)
        permissionDelegate = ArgumentCaptor.forClass(GeckoSession.PermissionDelegate::class.java)
113
        contentBlockingDelegate = ArgumentCaptor.forClass(ContentBlocking.Delegate::class.java)
114
        historyDelegate = ArgumentCaptor.forClass(GeckoSession.HistoryDelegate::class.java)
115
116
117
118
119
120
121
122
123
124

        geckoSession = mockGeckoSession()
        geckoSessionProvider = { geckoSession }
    }

    private fun captureDelegates() {
        verify(geckoSession).navigationDelegate = navigationDelegate.capture()
        verify(geckoSession).progressDelegate = progressDelegate.capture()
        verify(geckoSession).contentDelegate = contentDelegate.capture()
        verify(geckoSession).permissionDelegate = permissionDelegate.capture()
125
        verify(geckoSession).contentBlockingDelegate = contentBlockingDelegate.capture()
126
        verify(geckoSession).historyDelegate = historyDelegate.capture()
127
128
    }

129
    @Test
130
    fun engineSessionInitialization() {
131
        val runtime = mock<GeckoRuntime>()
132
        GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
133

134
135
136
137
138
139
        verify(geckoSession).open(any())

        captureDelegates()

        assertNotNull(navigationDelegate.value)
        assertNotNull(progressDelegate.value)
140
141
    }

142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
    @Test
    fun isIgnoredForTrackingProtection() {
        val mockedRuntime = mock<GeckoRuntime>()
        val mockedContentBlockingController = mock<ContentBlockingController>()
        var geckoResult = GeckoResult<Boolean?>()
        val session = GeckoEngineSession(mockedRuntime, geckoSessionProvider = geckoSessionProvider)
        var wasExecuted = false

        whenever(mockedRuntime.contentBlockingController).thenReturn(mockedContentBlockingController)
        whenever(mockedContentBlockingController.checkException(any())).thenReturn(geckoResult)

        session.isIgnoredForTrackingProtection {
            wasExecuted = it
        }

        geckoResult.complete(true)
        assertTrue(wasExecuted)

        geckoResult = GeckoResult()
        whenever(mockedContentBlockingController.checkException(any())).thenReturn(geckoResult)

        session.isIgnoredForTrackingProtection {
            wasExecuted = it
        }

        geckoResult.complete(null)
        assertFalse(wasExecuted)
    }

171
    @Test
172
    fun progressDelegateNotifiesObservers() {
173
        val engineSession = GeckoEngineSession(mock(),
174
                geckoSessionProvider = geckoSessionProvider)
175
176
177
178
179
180
181
182
183
184
185
186
187
188

        var observedProgress = 0
        var observedLoadingState = false
        var observedSecurityChange = false
        engineSession.register(object : EngineSession.Observer {
            override fun onLoadingStateChange(loading: Boolean) { observedLoadingState = loading }
            override fun onProgress(progress: Int) { observedProgress = progress }
            override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?) {
                // We cannot assert on actual parameters as SecurityInfo's fields can't be set
                // from the outside and its constructor isn't accessible either.
                observedSecurityChange = true
            }
        })

189
190
        captureDelegates()

191
        progressDelegate.value.onPageStart(mock(), "http://mozilla.org")
192
193
194
        assertEquals(GeckoEngineSession.PROGRESS_START, observedProgress)
        assertEquals(true, observedLoadingState)

195
        progressDelegate.value.onPageStop(mock(), true)
196
197
198
        assertEquals(GeckoEngineSession.PROGRESS_STOP, observedProgress)
        assertEquals(false, observedLoadingState)

199
200
201
202
203
204
205
206
207
208
        // Stop will update the loading state and progress observers even when
        // we haven't completed been successful.
        progressDelegate.value.onPageStart(mock(), "http://mozilla.org")
        assertEquals(GeckoEngineSession.PROGRESS_START, observedProgress)
        assertEquals(true, observedLoadingState)

        progressDelegate.value.onPageStop(mock(), false)
        assertEquals(GeckoEngineSession.PROGRESS_STOP, observedProgress)
        assertEquals(false, observedLoadingState)

209
        val securityInfo = mock<SecurityInformation>()
210
        progressDelegate.value.onSecurityChange(mock(), securityInfo)
211
212
213
214
        assertTrue(observedSecurityChange)

        observedSecurityChange = false

215
        progressDelegate.value.onSecurityChange(mock(), mock())
216
217
218
219
        assertTrue(observedSecurityChange)
    }

    @Test
220
    fun navigationDelegateNotifiesObservers() {
221
222
223
224
        val geckoResult = GeckoResult<Boolean?>()
        val mockedRuntime = mock<GeckoRuntime>()
        val mockedContentBlockingController = mock<ContentBlockingController>()
        val engineSession = GeckoEngineSession(mockedRuntime,
225
                geckoSessionProvider = geckoSessionProvider)
226
227

        var observedUrl = ""
228
229
        var observedCanGoBack = false
        var observedCanGoForward = false
230
231
232
233
234
235
236
237
        engineSession.register(object : EngineSession.Observer {
            override fun onLocationChange(url: String) { observedUrl = url }
            override fun onNavigationStateChange(canGoBack: Boolean?, canGoForward: Boolean?) {
                canGoBack?.let { observedCanGoBack = canGoBack }
                canGoForward?.let { observedCanGoForward = canGoForward }
            }
        })

238
239
240
        whenever(mockedRuntime.contentBlockingController).thenReturn(mockedContentBlockingController)
        whenever(mockedContentBlockingController.checkException(any())).thenReturn(geckoResult)

241
242
        captureDelegates()

243
244
        geckoResult.complete(true)

245
        navigationDelegate.value.onLocationChange(mock(), "http://mozilla.org")
246
        assertEquals("http://mozilla.org", observedUrl)
247
        verify(mockedContentBlockingController).checkException(any())
248

249
        navigationDelegate.value.onCanGoBack(mock(), true)
250
251
        assertEquals(true, observedCanGoBack)

252
        navigationDelegate.value.onCanGoForward(mock(), true)
253
254
255
        assertEquals(true, observedCanGoForward)
    }

256
    @Test
257
    fun contentDelegateNotifiesObserverAboutDownloads() {
258
        val engineSession = GeckoEngineSession(mock(),
259
                geckoSessionProvider = geckoSessionProvider)
260
261
262
263

        val observer: EngineSession.Observer = mock()
        engineSession.register(observer)

264
        val info: GeckoSession.WebResponseInfo = MockWebResponseInfo(
265
266
267
268
269
270
            uri = "https://download.mozilla.org",
            contentLength = 42,
            contentType = "image/png",
            filename = "image.png"
        )

271
272
        captureDelegates()
        contentDelegate.value.onExternalResponse(mock(), info)
273
274
275
276
277
278
279
280
281
282

        verify(observer).onExternalResource(
            url = "https://download.mozilla.org",
            fileName = "image.png",
            contentLength = 42,
            contentType = "image/png",
            userAgent = null,
            cookie = null)
    }

283
284
    @Test
    fun contentDelegateNotifiesObserverAboutWebAppManifest() {
285
        val engineSession = GeckoEngineSession(mock(),
286
287
288
289
290
            geckoSessionProvider = geckoSessionProvider)

        val observer: EngineSession.Observer = mock()
        engineSession.register(observer)

291
292
293
294
295
296
297
298
        val json = JSONObject().apply {
            put("name", "Minimal")
            put("start_url", "/")
        }
        val manifest = WebAppManifest(
            name = "Minimal",
            startUrl = "/"
        )
299
300

        captureDelegates()
301
        contentDelegate.value.onWebAppManifest(mock(), json)
302
303
304
305

        verify(observer).onWebAppManifestLoaded(manifest)
    }

306
307
    @Test
    fun permissionDelegateNotifiesObservers() {
308
        val engineSession = GeckoEngineSession(mock(),
309
                geckoSessionProvider = geckoSessionProvider)
310

311
312
        val observedContentPermissionRequests: MutableList<PermissionRequest> = mutableListOf()
        val observedAppPermissionRequests: MutableList<PermissionRequest> = mutableListOf()
313
314
315
316
317
318
319
320
321
322
        engineSession.register(object : EngineSession.Observer {
            override fun onContentPermissionRequest(permissionRequest: PermissionRequest) {
                observedContentPermissionRequests.add(permissionRequest)
            }

            override fun onAppPermissionRequest(permissionRequest: PermissionRequest) {
                observedAppPermissionRequests.add(permissionRequest)
            }
        })

323
324
325
326
        captureDelegates()

        permissionDelegate.value.onContentPermissionRequest(
            geckoSession,
327
328
            "originContent",
            GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION,
329
            mock()
330
331
        )

332
333
        permissionDelegate.value.onContentPermissionRequest(
            geckoSession,
334
335
            null,
            GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION,
336
            mock()
337
338
        )

339
340
        permissionDelegate.value.onMediaPermissionRequest(
            geckoSession,
341
342
343
            "originMedia",
            emptyArray(),
            emptyArray(),
344
            mock()
345
346
        )

347
348
        permissionDelegate.value.onMediaPermissionRequest(
            geckoSession,
349
            "about:blank",
350
351
            null,
            null,
352
            mock()
353
354
        )

355
356
        permissionDelegate.value.onAndroidPermissionsRequest(
            geckoSession,
357
            emptyArray(),
358
            mock()
359
360
        )

361
362
        permissionDelegate.value.onAndroidPermissionsRequest(
            geckoSession,
363
            null,
364
            mock()
365
366
367
368
369
370
        )

        assertEquals(4, observedContentPermissionRequests.size)
        assertEquals("originContent", observedContentPermissionRequests[0].uri)
        assertEquals("", observedContentPermissionRequests[1].uri)
        assertEquals("originMedia", observedContentPermissionRequests[2].uri)
371
        assertEquals("about:blank", observedContentPermissionRequests[3].uri)
372
373
374
        assertEquals(2, observedAppPermissionRequests.size)
    }

375
    @Test
376
    fun loadUrl() {
377
378
        val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
        val parentEngineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
379
380

        engineSession.loadUrl("http://mozilla.org")
381
        verify(geckoSession).loadUri("http://mozilla.org", null as GeckoSession?, GeckoSession.LOAD_FLAGS_NONE)
382

383
384
385
386
387
388
389
390
391
        engineSession.loadUrl("http://www.mozilla.org", flags = LoadUrlFlags.select(LoadUrlFlags.EXTERNAL))
        verify(geckoSession).loadUri("http://www.mozilla.org", null as GeckoSession?, GeckoSession.LOAD_FLAGS_EXTERNAL)

        engineSession.loadUrl("http://www.mozilla.org", parent = parentEngineSession)
        verify(geckoSession).loadUri(
            "http://www.mozilla.org",
            parentEngineSession.geckoSession,
            GeckoSession.LOAD_FLAGS_NONE
        )
392
393
    }

394
    @Test
395
    fun loadData() {
396
        val engineSession = GeckoEngineSession(mock(),
397
                geckoSessionProvider = geckoSessionProvider)
398
399

        engineSession.loadData("<html><body>Hello!</body></html>")
400
        verify(geckoSession).loadString(any(), eq("text/html"))
401

402
        engineSession.loadData("Hello!", "text/plain", "UTF-8")
403
        verify(geckoSession).loadString(any(), eq("text/plain"))
404
405

        engineSession.loadData("ahr0cdovl21vemlsbgeub3jn==", "text/plain", "base64")
406
        verify(geckoSession).loadData(any(), eq("text/plain"))
407
408

        engineSession.loadData("ahr0cdovl21vemlsbgeub3jn==", encoding = "base64")
409
        verify(geckoSession).loadData(any(), eq("text/html"))
410
411
412
    }

    @Test
413
    fun loadDataBase64() {
414
        val engineSession = GeckoEngineSession(mock(),
415
                geckoSessionProvider = geckoSessionProvider)
416
417
418
419
420
421
422
423
424

        engineSession.loadData("Hello!", "text/plain", "UTF-8")
        verify(geckoSession).loadString(eq("Hello!"), anyString())

        engineSession.loadData("ahr0cdovl21vemlsbgeub3jn==", "text/plain", "base64")
        verify(geckoSession).loadData(eq("ahr0cdovl21vemlsbgeub3jn==".toByteArray()), eq("text/plain"))

        engineSession.loadData("ahr0cdovl21vemlsbgeub3jn==", encoding = "base64")
        verify(geckoSession).loadData(eq("ahr0cdovl21vemlsbgeub3jn==".toByteArray()), eq("text/plain"))
425
426
    }

427
    @Test
428
    fun stopLoading() {
429
        val engineSession = GeckoEngineSession(mock(),
430
                geckoSessionProvider = geckoSessionProvider)
431
432

        engineSession.stopLoading()
433
434

        verify(geckoSession).stop()
435
436
    }

437
    @Test
438
    fun reload() {
439
        val engineSession = GeckoEngineSession(mock(),
440
                geckoSessionProvider = geckoSessionProvider)
441
442
443
        engineSession.loadUrl("http://mozilla.org")

        engineSession.reload()
444
445

        verify(geckoSession).reload()
446
447
448
    }

    @Test
449
    fun goBack() {
450
        val engineSession = GeckoEngineSession(mock(),
451
                geckoSessionProvider = geckoSessionProvider)
452
453

        engineSession.goBack()
454
455

        verify(geckoSession).goBack()
456
457
458
    }

    @Test
459
    fun goForward() {
460
        val engineSession = GeckoEngineSession(mock(),
461
                geckoSessionProvider = geckoSessionProvider)
462
463

        engineSession.goForward()
464
465

        verify(geckoSession).goForward()
466
467
468
    }

    @Test
469
    fun restoreState() {
470
        val engineSession = GeckoEngineSession(mock(),
471
                geckoSessionProvider = geckoSessionProvider)
472

473
474
475
476
        val actualState: GeckoSession.SessionState = mock()
        val state = GeckoEngineSessionState(actualState)

        engineSession.restoreState(state)
477
        verify(geckoSession).restoreState(any())
478
    }
479

480
481
    @Test
    fun `restoreState does nothing for null state`() {
482
        val engineSession = GeckoEngineSession(mock(),
483
484
485
486
487
488
489
490
            geckoSessionProvider = geckoSessionProvider)

        val state = GeckoEngineSessionState(null)

        engineSession.restoreState(state)
        verify(geckoSession, never()).restoreState(any())
    }

491
492
493
494
495
496
    class MockSecurityInformation(origin: String) : SecurityInformation() {
        init {
            ReflectionUtils.setField(this, "origin", origin)
        }
    }

497
    @Test
498
    fun progressDelegateIgnoresInitialLoadOfAboutBlank() {
499
        val engineSession = GeckoEngineSession(mock(),
500
                geckoSessionProvider = geckoSessionProvider)
501
502
503
504
505
506
507
508

        var observedSecurityChange = false
        engineSession.register(object : EngineSession.Observer {
            override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?) {
                observedSecurityChange = true
            }
        })

509
510
        captureDelegates()

511
        progressDelegate.value.onSecurityChange(mock(),
512
                MockSecurityInformation("moz-nullprincipal:{uuid}"))
513
514
        assertFalse(observedSecurityChange)

515
        progressDelegate.value.onSecurityChange(mock(),
516
                MockSecurityInformation("https://www.mozilla.org"))
517
518
519
520
        assertTrue(observedSecurityChange)
    }

    @Test
521
    fun navigationDelegateIgnoresInitialLoadOfAboutBlank() {
522
523
524
525
        val geckoResult = GeckoResult<Boolean?>()
        val mockedRuntime = mock<GeckoRuntime>()
        val mockedContentBlockingController = mock<ContentBlockingController>()
        val engineSession = GeckoEngineSession(mockedRuntime,
526
                geckoSessionProvider = geckoSessionProvider)
527
528
529
530
531
532

        var observedUrl = ""
        engineSession.register(object : EngineSession.Observer {
            override fun onLocationChange(url: String) { observedUrl = url }
        })

533
534
535
        whenever(mockedRuntime.contentBlockingController).thenReturn(mockedContentBlockingController)
        whenever(mockedContentBlockingController.checkException(any())).thenReturn(geckoResult)

536
537
        captureDelegates()

538
539
        geckoResult.complete(true)

540
        navigationDelegate.value.onLocationChange(mock(), "about:blank")
541
542
        assertEquals("", observedUrl)

543
        navigationDelegate.value.onLocationChange(mock(), "about:blank")
544
545
        assertEquals("", observedUrl)

546
        navigationDelegate.value.onLocationChange(mock(), "https://www.mozilla.org")
547
        assertEquals("https://www.mozilla.org", observedUrl)
548
        verify(mockedContentBlockingController).checkException(any())
549

550
        navigationDelegate.value.onLocationChange(mock(), "about:blank")
551
552
        assertEquals("about:blank", observedUrl)
    }
553

Grisha Kruglov's avatar
Grisha Kruglov committed
554
    @Test
555
    fun `keeps track of current url via onPageStart events`() {
556
        val engineSession = GeckoEngineSession(mock(),
557
558
559
                geckoSessionProvider = geckoSessionProvider)

        captureDelegates()
Grisha Kruglov's avatar
Grisha Kruglov committed
560
561

        assertNull(engineSession.currentUrl)
562
        progressDelegate.value.onPageStart(geckoSession, "https://www.mozilla.org")
Grisha Kruglov's avatar
Grisha Kruglov committed
563
564
        assertEquals("https://www.mozilla.org", engineSession.currentUrl)

565
        progressDelegate.value.onPageStart(geckoSession, "https://www.firefox.com")
Grisha Kruglov's avatar
Grisha Kruglov committed
566
567
568
569
        assertEquals("https://www.firefox.com", engineSession.currentUrl)
    }

    @Test
570
571
    fun `notifies configured history delegate of title changes`() = runBlockingTest {
        val engineSession = GeckoEngineSession(mock(),
572
                geckoSessionProvider = geckoSessionProvider,
573
            context = coroutineContext)
574
        val historyTrackingDelegate: HistoryTrackingDelegate = mock()
Grisha Kruglov's avatar
Grisha Kruglov committed
575

576
577
        captureDelegates()

Grisha Kruglov's avatar
Grisha Kruglov committed
578
        // Nothing breaks if history delegate isn't configured.
579
        contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
Grisha Kruglov's avatar
Grisha Kruglov committed
580

581
        engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
Grisha Kruglov's avatar
Grisha Kruglov committed
582

583
        contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
584
        verify(historyTrackingDelegate, never()).onTitleChanged(anyString(), anyString())
Grisha Kruglov's avatar
Grisha Kruglov committed
585
586

        // This sets the currentUrl.
587
        progressDelegate.value.onPageStart(geckoSession, "https://www.mozilla.com")
Grisha Kruglov's avatar
Grisha Kruglov committed
588

589
        contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
590
        verify(historyTrackingDelegate).onTitleChanged(eq("https://www.mozilla.com"), eq("Hello World!"))
Grisha Kruglov's avatar
Grisha Kruglov committed
591
592
593
    }

    @Test
594
595
    fun `does not notify configured history delegate of title changes for private sessions`() = runBlockingTest {
        val engineSession = GeckoEngineSession(mock(),
596
                geckoSessionProvider = geckoSessionProvider,
597
            context = coroutineContext,
598
599
                privateMode = true)
        val historyTrackingDelegate: HistoryTrackingDelegate = mock()
Grisha Kruglov's avatar
Grisha Kruglov committed
600

601
602
        captureDelegates()

Grisha Kruglov's avatar
Grisha Kruglov committed
603
        // Nothing breaks if history delegate isn't configured.
604
        contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
Grisha Kruglov's avatar
Grisha Kruglov committed
605

606
607
608
609
        engineSession.settings.historyTrackingDelegate = historyTrackingDelegate

        val observer: EngineSession.Observer = mock()
        engineSession.register(observer)
Grisha Kruglov's avatar
Grisha Kruglov committed
610

611
        contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
612
613
        verify(historyTrackingDelegate, never()).onTitleChanged(anyString(), anyString())
        verify(observer).onTitleChange("Hello World!")
Grisha Kruglov's avatar
Grisha Kruglov committed
614
615

        // This sets the currentUrl.
616
        progressDelegate.value.onPageStart(geckoSession, "https://www.mozilla.com")
Grisha Kruglov's avatar
Grisha Kruglov committed
617

618
619
620
621
622
623
        contentDelegate.value.onTitleChange(geckoSession, "Mozilla")
        verify(historyTrackingDelegate, never()).onTitleChanged(anyString(), anyString())
        verify(observer).onTitleChange("Mozilla")
    }

    @Test
624
625
    fun `does not notify configured history delegate for redirects`() = runBlockingTest {
        val engineSession = GeckoEngineSession(mock(),
626
                geckoSessionProvider = geckoSessionProvider,
627
            context = coroutineContext)
628
629
630
631
632
633
634
635
636
637
638
639
        val historyTrackingDelegate: HistoryTrackingDelegate = mock()

        captureDelegates()

        // Nothing breaks if history delegate isn't configured.
        historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com", null, GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)
            engineSession.job.children.forEach { it.join() }

        engineSession.settings.historyTrackingDelegate = historyTrackingDelegate

        historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com", null, GeckoSession.HistoryDelegate.VISIT_REDIRECT_TEMPORARY)
            engineSession.job.children.forEach { it.join() }
640
            verify(historyTrackingDelegate, never()).onVisited(anyString(), any())
641
642
643
    }

    @Test
644
645
    fun `does not notify configured history delegate for top-level visits to error pages`() = runBlockingTest {
        val engineSession = GeckoEngineSession(mock(),
646
                geckoSessionProvider = geckoSessionProvider,
647
            context = coroutineContext)
648
649
650
651
652
653
654
655
        val historyTrackingDelegate: HistoryTrackingDelegate = mock()

        captureDelegates()

        engineSession.settings.historyTrackingDelegate = historyTrackingDelegate

        historyDelegate.value.onVisited(geckoSession, "about:neterror", null, GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL or GeckoSession.HistoryDelegate.VISIT_UNRECOVERABLE_ERROR)
            engineSession.job.children.forEach { it.join() }
656
            verify(historyTrackingDelegate, never()).onVisited(anyString(), any())
657
658
659
    }

    @Test
660
661
    fun `notifies configured history delegate of visits`() = runBlockingTest {
        val engineSession = GeckoEngineSession(mock(),
662
                geckoSessionProvider = geckoSessionProvider,
663
            context = coroutineContext)
664
665
666
667
668
        val historyTrackingDelegate: HistoryTrackingDelegate = mock()

        captureDelegates()

        engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
669
        whenever(historyTrackingDelegate.shouldStoreUri("https://www.mozilla.com")).thenReturn(true)
670
671
672

        historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com", null, GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)
            engineSession.job.children.forEach { it.join() }
673
            verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com"), eq(PageVisit(VisitType.LINK, RedirectSource.NOT_A_SOURCE)))
674
675
676
    }

    @Test
677
678
    fun `notifies configured history delegate of reloads`() = runBlockingTest {
        val engineSession = GeckoEngineSession(mock(),
679
                geckoSessionProvider = geckoSessionProvider,
680
            context = coroutineContext)
681
682
683
684
685
        val historyTrackingDelegate: HistoryTrackingDelegate = mock()

        captureDelegates()

        engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
686
        whenever(historyTrackingDelegate.shouldStoreUri("https://www.mozilla.com")).thenReturn(true)
687
688
689

        historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com", "https://www.mozilla.com", GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)
            engineSession.job.children.forEach { it.join() }
690
            verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com"), eq(PageVisit(VisitType.RELOAD, RedirectSource.NOT_A_SOURCE)))
691
692
693
    }

    @Test
694
695
    fun `checks with the delegate before trying to record a visit`() = runBlockingTest {
        val engineSession = GeckoEngineSession(mock(),
696
                geckoSessionProvider = geckoSessionProvider,
697
            context = coroutineContext)
698
699
700
701
702
        val historyTrackingDelegate: HistoryTrackingDelegate = mock()

        captureDelegates()

        engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
703
704
        whenever(historyTrackingDelegate.shouldStoreUri("https://www.mozilla.com/allowed")).thenReturn(true)
        whenever(historyTrackingDelegate.shouldStoreUri("https://www.mozilla.com/not-allowed")).thenReturn(false)
705
706
707
708
709

        historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com/allowed", null, GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)

            engineSession.job.children.forEach { it.join() }
            verify(historyTrackingDelegate).shouldStoreUri("https://www.mozilla.com/allowed")
710
            verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com/allowed"), eq(PageVisit(VisitType.LINK, RedirectSource.NOT_A_SOURCE)))
711
712
713
714
715

        historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com/not-allowed", null, GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)

            engineSession.job.children.forEach { it.join() }
            verify(historyTrackingDelegate).shouldStoreUri("https://www.mozilla.com/not-allowed")
716
        verify(historyTrackingDelegate, never()).onVisited(eq("https://www.mozilla.com/not-allowed"), any())
717
718
719
    }

    @Test
720
721
    fun `correctly processes redirect visit flags`() = runBlockingTest {
        val engineSession = GeckoEngineSession(mock(),
722
                geckoSessionProvider = geckoSessionProvider,
723
            context = coroutineContext)
724
725
726
727
728
        val historyTrackingDelegate: HistoryTrackingDelegate = mock()

        captureDelegates()

        engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
729
        whenever(historyTrackingDelegate.shouldStoreUri(any())).thenReturn(true)
730
731
732
733
734
735
736
737
738
739
740

        historyDelegate.value.onVisited(
                geckoSession,
                "https://www.mozilla.com/tempredirect",
                null,
                // bitwise 'or'
                GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL
                        or GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE
        )

            engineSession.job.children.forEach { it.join() }
741
            verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com/tempredirect"), eq(PageVisit(VisitType.LINK, RedirectSource.TEMPORARY)))
742
743
744
745
746
747
748
749
750
751

        historyDelegate.value.onVisited(
                geckoSession,
                "https://www.mozilla.com/permredirect",
                null,
                GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL
                        or GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE_PERMANENT
        )

            engineSession.job.children.forEach { it.join() }
752
            verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com/permredirect"), eq(PageVisit(VisitType.LINK, RedirectSource.PERMANENT)))
753
754
755
756
757
758
759
760
761
762
763
764

        // Visits below are targets of redirects, not redirects themselves.
        // Check that they're mapped to "link".
        historyDelegate.value.onVisited(
                geckoSession,
                "https://www.mozilla.com/targettemp",
                null,
                GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL
                        or GeckoSession.HistoryDelegate.VISIT_REDIRECT_TEMPORARY
        )

            engineSession.job.children.forEach { it.join() }
765
            verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com/targettemp"), eq(PageVisit(VisitType.REDIRECT_TEMPORARY, RedirectSource.NOT_A_SOURCE)))
766
767
768
769
770
771

        historyDelegate.value.onVisited(
                geckoSession,
                "https://www.mozilla.com/targetperm",
                null,
                GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL
772
                        or GeckoSession.HistoryDelegate.VISIT_REDIRECT_PERMANENT
773
774
775
        )

            engineSession.job.children.forEach { it.join() }
776
            verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com/targetperm"), eq(PageVisit(VisitType.REDIRECT_PERMANENT, RedirectSource.NOT_A_SOURCE)))
777
778
779
    }

    @Test
780
781
    fun `does not notify configured history delegate of visits for private sessions`() = runBlockingTest {
        val engineSession = GeckoEngineSession(mock(),
782
                geckoSessionProvider = geckoSessionProvider,
783
            context = coroutineContext,
784
785
786
787
788
789
790
791
792
                privateMode = true)
        val historyTrackingDelegate: HistoryTrackingDelegate = mock()

        captureDelegates()

        engineSession.settings.historyTrackingDelegate = historyTrackingDelegate

        historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com", "https://www.mozilla.com", GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)
            engineSession.job.children.forEach { it.join() }
793
            verify(historyTrackingDelegate, never()).onVisited(anyString(), any())
794
795
796
    }

    @Test
797
798
    fun `requests visited URLs from configured history delegate`() = runBlockingTest {
        val engineSession = GeckoEngineSession(mock(),
799
                geckoSessionProvider = geckoSessionProvider,
800
            context = coroutineContext)
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
        val historyTrackingDelegate: HistoryTrackingDelegate = mock()

        captureDelegates()

        // Nothing breaks if history delegate isn't configured.
        historyDelegate.value.getVisited(geckoSession, arrayOf("https://www.mozilla.com", "https://www.mozilla.org"))
            engineSession.job.children.forEach { it.join() }

        engineSession.settings.historyTrackingDelegate = historyTrackingDelegate

        historyDelegate.value.getVisited(geckoSession, arrayOf("https://www.mozilla.com", "https://www.mozilla.org"))
            engineSession.job.children.forEach { it.join() }
            verify(historyTrackingDelegate).getVisited(eq(listOf("https://www.mozilla.com", "https://www.mozilla.org")))
    }

    @Test
817
818
    fun `does not request visited URLs from configured history delegate in private sessions`() = runBlockingTest {
        val engineSession = GeckoEngineSession(mock(),
819
                geckoSessionProvider = geckoSessionProvider,
820
            context = coroutineContext,
821
822
823
824
825
826
827
828
829
830
                privateMode = true)
        val historyTrackingDelegate: HistoryTrackingDelegate = mock()

        captureDelegates()

        engineSession.settings.historyTrackingDelegate = historyTrackingDelegate

        historyDelegate.value.getVisited(geckoSession, arrayOf("https://www.mozilla.com", "https://www.mozilla.org"))
            engineSession.job.children.forEach { it.join() }
            verify(historyTrackingDelegate, never()).getVisited(anyList())
Grisha Kruglov's avatar
Grisha Kruglov committed
831
832
    }

833
    @Test
834
    fun websiteTitleUpdates() {
835
        val engineSession = GeckoEngineSession(mock(),
836
                geckoSessionProvider = geckoSessionProvider)
837
838
839
840

        val observer: EngineSession.Observer = mock()
        engineSession.register(observer)

841
842
843
        captureDelegates()

        contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
844
845
846

        verify(observer).onTitleChange("Hello World!")
    }
847
848

    @Test
849
    fun trackingProtectionDelegateNotifiesObservers() {
850
851
852
853
        val engineSession = GeckoEngineSession(
            mock(),
            geckoSessionProvider = geckoSessionProvider
        )
854

855
        var trackerBlocked: Tracker? = null
856
        engineSession.register(object : EngineSession.Observer {
857
858
            override fun onTrackerBlocked(tracker: Tracker) {
                trackerBlocked = tracker
859
860
861
            }
        })

862
        captureDelegates()
863
864
865
866
867
868
869
870
871
872
873
874
875
876
        var geckoCategories = 0
        geckoCategories = geckoCategories.or(GeckoAntiTracking.AD)
        geckoCategories = geckoCategories.or(GeckoAntiTracking.ANALYTIC)
        geckoCategories = geckoCategories.or(GeckoAntiTracking.SOCIAL)
        geckoCategories = geckoCategories.or(GeckoAntiTracking.CRYPTOMINING)
        geckoCategories = geckoCategories.or(GeckoAntiTracking.FINGERPRINTING)
        geckoCategories = geckoCategories.or(GeckoAntiTracking.CONTENT)
        geckoCategories = geckoCategories.or(GeckoAntiTracking.TEST)

        contentBlockingDelegate.value.onContentBlocked(
            geckoSession,
            ContentBlocking.BlockEvent("tracker1", geckoCategories, 0, 0, false)
        )

877
        assertEquals("tracker1", trackerBlocked!!.url)
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931

        val expectedBlockedCategories = listOf(
            TrackingCategory.AD,
            TrackingCategory.ANALYTICS,
            TrackingCategory.SOCIAL,
            TrackingCategory.CRYPTOMINING,
            TrackingCategory.FINGERPRINTING,
            TrackingCategory.CONTENT,
            TrackingCategory.TEST
        )

        assertTrue(trackerBlocked!!.trackingCategories.containsAll(expectedBlockedCategories))

        var trackerLoaded: Tracker? = null
        engineSession.register(object : EngineSession.Observer {
            override fun onTrackerLoaded(tracker: Tracker) {
                trackerLoaded = tracker
            }
        })

        var geckoCookieCategories = 0
        geckoCookieCategories = geckoCookieCategories.or(GeckoCookieBehavior.ACCEPT_ALL)
        geckoCookieCategories = geckoCookieCategories.or(GeckoCookieBehavior.ACCEPT_VISITED)
        geckoCookieCategories = geckoCookieCategories.or(GeckoCookieBehavior.ACCEPT_NON_TRACKERS)
        geckoCookieCategories = geckoCookieCategories.or(GeckoCookieBehavior.ACCEPT_NONE)
        geckoCookieCategories = geckoCookieCategories.or(GeckoCookieBehavior.ACCEPT_FIRST_PARTY)

        contentBlockingDelegate.value.onContentLoaded(
            geckoSession,
            ContentBlocking.BlockEvent("tracker1", 0, 0, geckoCookieCategories, false)
        )

        val expectedCookieCategories = listOf(
            CookiePolicy.ACCEPT_ONLY_FIRST_PARTY,
            CookiePolicy.ACCEPT_NONE,
            CookiePolicy.ACCEPT_VISITED,
            CookiePolicy.ACCEPT_NON_TRACKERS
        )

        assertEquals("tracker1", trackerLoaded!!.url)
        assertTrue(trackerLoaded!!.cookiePolicies.containsAll(expectedCookieCategories))

        contentBlockingDelegate.value.onContentLoaded(
            geckoSession,
            ContentBlocking.BlockEvent("tracker1", 0, 0, GeckoCookieBehavior.ACCEPT_ALL, false)
        )

        assertTrue(
            trackerLoaded!!.cookiePolicies.containsAll(
                listOf(
                    CookiePolicy.ACCEPT_ALL
                )
            )
        )
932
933
934
    }

    @Test