Skip to content

Commit e973f81

Browse files
authored
PIR: Add unit tests for PirDashboardWebMessagingInterface (#6656)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1211134926977199?focus=true ### Description Adds unit tests for `PirDashboardWebMessagingInterface` ### Steps to test this PR Run tests in `PirDashboardWebMessagingInterface` ### UI changes No UI changes
1 parent 0191f64 commit e973f81

File tree

2 files changed

+378
-0
lines changed

2 files changed

+378
-0
lines changed

pir/pir-impl/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ dependencies {
6262
testImplementation "org.mockito.kotlin:mockito-kotlin:_"
6363
testImplementation project(path: ':common-test')
6464
testImplementation CashApp.turbine
65+
testImplementation AndroidX.test.ext.junit
6566
testImplementation Testing.robolectric
6667
testImplementation "androidx.lifecycle:lifecycle-runtime-testing:_"
6768
testImplementation(KotlinX.coroutines.test) {
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.pir.impl.dashboard.messaging
18+
19+
import android.webkit.WebView
20+
import androidx.test.ext.junit.runners.AndroidJUnit4
21+
import com.duckduckgo.common.test.CoroutineTestRule
22+
import com.duckduckgo.common.utils.plugins.PluginPoint
23+
import com.duckduckgo.js.messaging.api.JsCallbackData
24+
import com.duckduckgo.js.messaging.api.JsMessage
25+
import com.duckduckgo.js.messaging.api.JsMessageCallback
26+
import com.duckduckgo.js.messaging.api.JsMessageHelper
27+
import com.duckduckgo.js.messaging.api.JsRequestResponse
28+
import com.duckduckgo.js.messaging.api.SubscriptionEvent
29+
import com.duckduckgo.js.messaging.api.SubscriptionEventData
30+
import com.duckduckgo.pir.impl.dashboard.messaging.handlers.PirWebJsMessageHandler
31+
import kotlinx.coroutines.test.runTest
32+
import org.json.JSONObject
33+
import org.junit.Assert.assertEquals
34+
import org.junit.Before
35+
import org.junit.Rule
36+
import org.junit.Test
37+
import org.junit.runner.RunWith
38+
import org.mockito.kotlin.any
39+
import org.mockito.kotlin.argumentCaptor
40+
import org.mockito.kotlin.mock
41+
import org.mockito.kotlin.never
42+
import org.mockito.kotlin.verify
43+
import org.mockito.kotlin.whenever
44+
45+
@RunWith(AndroidJUnit4::class)
46+
class PirDashboardWebMessagingInterfaceTest {
47+
48+
@get:Rule
49+
val coroutineRule = CoroutineTestRule()
50+
51+
private lateinit var testee: PirDashboardWebMessagingInterface
52+
53+
private val mockJsMessageHelper: JsMessageHelper = mock()
54+
private val mockMessageHandlers: PluginPoint<PirWebJsMessageHandler> = mock()
55+
private val mockWebView: WebView = mock()
56+
private val mockJsMessageCallback: JsMessageCallback = mock()
57+
private val mockMessageHandler: PirWebJsMessageHandler = mock()
58+
private val mockJsonObject: JSONObject = mock()
59+
60+
@Before
61+
fun setUp() {
62+
testee = PirDashboardWebMessagingInterface(
63+
jsMessageHelper = mockJsMessageHelper,
64+
dispatcherProvider = coroutineRule.testDispatcherProvider,
65+
messageHandlers = mockMessageHandlers,
66+
)
67+
}
68+
69+
@Test
70+
fun whenRegisterWithValidCallbackThenWebViewIsConfigured() {
71+
// When
72+
testee.register(mockWebView, mockJsMessageCallback)
73+
74+
// Then
75+
verify(mockWebView).addJavascriptInterface(testee, "dbpui")
76+
}
77+
78+
@Test(expected = Exception::class)
79+
fun whenRegisterWithNullCallbackThenThrowsException() {
80+
// When
81+
testee.register(mockWebView, null)
82+
}
83+
84+
@Test
85+
fun whenProcessWithValidMessageAndSecretAndDomainThenDelegatesToHandler() = runTest {
86+
// Given
87+
val validSecret = PirDashboardWebConstants.SECRET
88+
val messageJson =
89+
"""{"context":"dbpui","featureName":"testFeature","method":"testMethod","id":"123","params":{}}"""
90+
91+
testee.register(mockWebView, mockJsMessageCallback)
92+
whenever(mockWebView.url).thenReturn("https://duckduckgo.com/test")
93+
whenever(mockMessageHandlers.getPlugins()).thenReturn(listOf(mockMessageHandler))
94+
whenever(mockMessageHandler.methods).thenReturn(listOf("testMethod"))
95+
whenever(mockMessageHandler.featureName).thenReturn("testFeature")
96+
97+
// When
98+
testee.process(messageJson, validSecret)
99+
100+
// Then
101+
val messageCaptor = argumentCaptor<JsMessage>()
102+
verify(mockMessageHandler).process(messageCaptor.capture(), any(), any())
103+
assertEquals("dbpui", messageCaptor.firstValue.context)
104+
assertEquals("testFeature", messageCaptor.firstValue.featureName)
105+
assertEquals("testMethod", messageCaptor.firstValue.method)
106+
}
107+
108+
@Test
109+
fun whenProcessWithInvalidSecretThenDoesNotDelegateToHandler() = runTest {
110+
// Given
111+
val messageJson =
112+
"""{"context":"dbpui","featureName":"testFeature","method":"testMethod","id":"123","params":{}}"""
113+
val invalidSecret = "invalid-secret"
114+
115+
testee.register(mockWebView, mockJsMessageCallback)
116+
whenever(mockWebView.url).thenReturn("https://duckduckgo.com/test")
117+
whenever(mockMessageHandlers.getPlugins()).thenReturn(listOf(mockMessageHandler))
118+
119+
// When
120+
testee.process(messageJson, invalidSecret)
121+
122+
// Then
123+
verify(mockMessageHandler, never()).process(any(), any(), any())
124+
}
125+
126+
@Test
127+
fun whenProcessWithInvalidContextThenDoesNotDelegateToHandler() = runTest {
128+
// Given
129+
val messageJson = """{"context":"invalidContext","featureName":"testFeature","method":"testMethod","id":"123","params":{}}"""
130+
val validSecret = PirDashboardWebConstants.SECRET
131+
132+
testee.register(mockWebView, mockJsMessageCallback)
133+
whenever(mockWebView.url).thenReturn("https://duckduckgo.com/test")
134+
whenever(mockMessageHandlers.getPlugins()).thenReturn(listOf(mockMessageHandler))
135+
136+
// When
137+
testee.process(messageJson, validSecret)
138+
139+
// Then
140+
verify(mockMessageHandler, never()).process(any(), any(), any())
141+
}
142+
143+
@Test
144+
fun whenProcessWithDisallowedDomainThenDoesNotDelegateToHandler() = runTest {
145+
// Given
146+
val messageJson =
147+
"""{"context":"dbpui","featureName":"testFeature","method":"testMethod","id":"123","params":{}}"""
148+
val validSecret = PirDashboardWebConstants.SECRET
149+
150+
testee.register(mockWebView, mockJsMessageCallback)
151+
whenever(mockWebView.url).thenReturn("https://malicious.com/test")
152+
whenever(mockMessageHandlers.getPlugins()).thenReturn(listOf(mockMessageHandler))
153+
154+
// When
155+
testee.process(messageJson, validSecret)
156+
157+
// Then
158+
verify(mockMessageHandler, never()).process(any(), any(), any())
159+
}
160+
161+
@Test
162+
fun whenProcessWithInvalidJsonThenDoesNotThrowException() = runTest {
163+
// Given
164+
val invalidJson = "invalid json"
165+
val validSecret = PirDashboardWebConstants.SECRET
166+
167+
testee.register(mockWebView, mockJsMessageCallback)
168+
169+
// When - This should not throw an exception
170+
testee.process(invalidJson, validSecret)
171+
172+
// Then
173+
verify(mockMessageHandler, never()).process(any(), any(), any())
174+
}
175+
176+
@Test
177+
fun whenProcessWithNoMatchingHandlerThenDoesNotProcess() = runTest {
178+
// Given
179+
val messageJson =
180+
"""{"context":"dbpui","featureName":"testFeature","method":"testMethod","id":"123","params":{}}"""
181+
val validSecret = PirDashboardWebConstants.SECRET
182+
183+
testee.register(mockWebView, mockJsMessageCallback)
184+
whenever(mockWebView.url).thenReturn("https://duckduckgo.com/test")
185+
whenever(mockMessageHandlers.getPlugins()).thenReturn(listOf(mockMessageHandler))
186+
whenever(mockMessageHandler.methods).thenReturn(listOf("differentMethod"))
187+
whenever(mockMessageHandler.featureName).thenReturn("testFeature")
188+
189+
// When
190+
testee.process(messageJson, validSecret)
191+
192+
// Then
193+
verify(mockMessageHandler, never()).process(any(), any(), any())
194+
}
195+
196+
@Test
197+
fun whenProcessWithMatchingMethodButDifferentFeatureThenDoesNotProcess() = runTest {
198+
// Given
199+
val messageJson =
200+
"""{"context":"dbpui","featureName":"testFeature","method":"testMethod","id":"123","params":{}}"""
201+
val validSecret = PirDashboardWebConstants.SECRET
202+
203+
testee.register(mockWebView, mockJsMessageCallback)
204+
whenever(mockWebView.url).thenReturn("https://duckduckgo.com/test")
205+
whenever(mockMessageHandlers.getPlugins()).thenReturn(listOf(mockMessageHandler))
206+
whenever(mockMessageHandler.methods).thenReturn(listOf("testMethod"))
207+
whenever(mockMessageHandler.featureName).thenReturn("differentFeature")
208+
209+
// When
210+
testee.process(messageJson, validSecret)
211+
212+
// Then
213+
verify(mockMessageHandler, never()).process(any(), any(), any())
214+
}
215+
216+
@Test
217+
fun whenOnResponseThenSendsJsResponse() {
218+
// Given
219+
val callbackData = JsCallbackData(
220+
featureName = "testFeature",
221+
method = "testMethod",
222+
id = "123",
223+
params = mockJsonObject,
224+
)
225+
226+
testee.register(mockWebView, mockJsMessageCallback)
227+
228+
// When
229+
testee.onResponse(callbackData)
230+
231+
// Then
232+
verify(mockJsMessageHelper).sendJsResponse(
233+
any<JsRequestResponse.Success>(),
234+
any(),
235+
any(),
236+
any(),
237+
)
238+
}
239+
240+
@Test
241+
fun whenOnResponseThenCreatesCorrectJsResponse() {
242+
// Given
243+
val callbackData = JsCallbackData(
244+
featureName = "testFeature",
245+
method = "testMethod",
246+
id = "123",
247+
params = mockJsonObject,
248+
)
249+
250+
testee.register(mockWebView, mockJsMessageCallback)
251+
252+
// When
253+
testee.onResponse(callbackData)
254+
255+
// Then
256+
verify(mockJsMessageHelper).sendJsResponse(
257+
argThat { response ->
258+
response is JsRequestResponse.Success &&
259+
response.context == "dbpui" &&
260+
response.featureName == "testFeature" &&
261+
response.method == "testMethod" &&
262+
response.id == "123" &&
263+
response.result == mockJsonObject
264+
},
265+
eq(PirDashboardWebConstants.MESSAGE_CALLBACK),
266+
eq(PirDashboardWebConstants.SECRET),
267+
eq(mockWebView),
268+
)
269+
}
270+
271+
@Test
272+
fun whenSendSubscriptionEventThenSendsEvent() {
273+
// Given
274+
val subscriptionEventData = SubscriptionEventData(
275+
featureName = "testFeature",
276+
subscriptionName = "testSubscription",
277+
params = mockJsonObject,
278+
)
279+
280+
testee.register(mockWebView, mockJsMessageCallback)
281+
282+
// When
283+
testee.sendSubscriptionEvent(subscriptionEventData)
284+
285+
// Then
286+
verify(mockJsMessageHelper).sendSubscriptionEvent(
287+
any<SubscriptionEvent>(),
288+
any(),
289+
any(),
290+
any(),
291+
)
292+
}
293+
294+
@Test
295+
fun whenSendSubscriptionEventThenCreatesCorrectEvent() {
296+
// Given
297+
val subscriptionEventData = SubscriptionEventData(
298+
featureName = "testFeature",
299+
subscriptionName = "testSubscription",
300+
params = mockJsonObject,
301+
)
302+
303+
testee.register(mockWebView, mockJsMessageCallback)
304+
305+
// When
306+
testee.sendSubscriptionEvent(subscriptionEventData)
307+
308+
// Then
309+
verify(mockJsMessageHelper).sendSubscriptionEvent(
310+
argThat { event ->
311+
event.context == "dbpui" &&
312+
event.featureName == "testFeature" &&
313+
event.subscriptionName == "testSubscription" &&
314+
event.params == mockJsonObject
315+
},
316+
eq(PirDashboardWebConstants.MESSAGE_CALLBACK),
317+
eq(PirDashboardWebConstants.SECRET),
318+
eq(mockWebView),
319+
)
320+
}
321+
322+
@Test
323+
fun whenInstanceCreatedThenPropertiesAreSetCorrectly() {
324+
// Then
325+
assertEquals(PirDashboardWebConstants.SCRIPT_CONTEXT_NAME, testee.context)
326+
assertEquals(PirDashboardWebConstants.MESSAGE_CALLBACK, testee.callbackName)
327+
assertEquals(PirDashboardWebConstants.SECRET, testee.secret)
328+
assertEquals(listOf(PirDashboardWebConstants.ALLOWED_DOMAIN), testee.allowedDomains)
329+
}
330+
331+
@Test
332+
fun whenProcessWithNullUrlThenDoesNotDelegateToHandler() = runTest {
333+
// Given
334+
val messageJson =
335+
"""{"context":"dbpui","featureName":"testFeature","method":"testMethod","id":"123","params":{}}"""
336+
val validSecret = PirDashboardWebConstants.SECRET
337+
338+
testee.register(mockWebView, mockJsMessageCallback)
339+
whenever(mockWebView.url).thenReturn(null)
340+
whenever(mockMessageHandlers.getPlugins()).thenReturn(listOf(mockMessageHandler))
341+
342+
// When
343+
testee.process(messageJson, validSecret)
344+
345+
// Then
346+
verify(mockMessageHandler, never()).process(any(), any(), any())
347+
}
348+
349+
@Test
350+
fun whenProcessWithSubdomainOfAllowedDomainThenDelegatesToHandler() = runTest {
351+
// Given
352+
val messageJson =
353+
"""{"context":"dbpui","featureName":"testFeature","method":"testMethod","id":"123","params":{}}"""
354+
val validSecret = PirDashboardWebConstants.SECRET
355+
356+
testee.register(mockWebView, mockJsMessageCallback)
357+
whenever(mockWebView.url).thenReturn("https://sub.duckduckgo.com/test")
358+
whenever(mockMessageHandlers.getPlugins()).thenReturn(listOf(mockMessageHandler))
359+
whenever(mockMessageHandler.methods).thenReturn(listOf("testMethod"))
360+
whenever(mockMessageHandler.featureName).thenReturn("testFeature")
361+
362+
// When
363+
testee.process(messageJson, validSecret)
364+
365+
// Then
366+
val messageCaptor = argumentCaptor<JsMessage>()
367+
verify(mockMessageHandler).process(messageCaptor.capture(), any(), any())
368+
assertEquals("testFeature", messageCaptor.firstValue.featureName)
369+
assertEquals("testMethod", messageCaptor.firstValue.method)
370+
}
371+
372+
private inline fun <reified T : Any> argThat(noinline predicate: (T) -> Boolean): T {
373+
return org.mockito.kotlin.argThat(predicate)
374+
}
375+
376+
private fun <T> eq(value: T): T = org.mockito.kotlin.eq(value)
377+
}

0 commit comments

Comments
 (0)