@@ -7,11 +7,13 @@ import android.net.Uri
77import android.os.Looper
88import androidx.activity.ComponentActivity
99import org.assertj.core.api.Assertions.assertThat
10+ import org.junit.Assert.fail
1011import org.junit.Before
1112import org.junit.Test
1213import org.junit.runner.RunWith
1314import org.mockito.Mockito
1415import org.mockito.kotlin.any
16+ import org.mockito.kotlin.argThat
1517import org.mockito.kotlin.argumentCaptor
1618import org.mockito.kotlin.isNull
1719import org.mockito.kotlin.mock
@@ -360,20 +362,121 @@ class EmbeddedCheckoutProtocolTest {
360362
361363 // endregion
362364
363- // region delegated notifications
365+ // region ec.error — severity-driven dismissal
366+
367+ @Test
368+ fun `ec error is forwarded to client regardless of severity` () {
369+ val rawMessage = ecErrorMessage(severity = " recoverable" )
370+ val client = mock<CheckoutCommunicationClient >()
371+ ecp.setClient(client)
372+
373+ ecp.postMessage(rawMessage)
374+ shadowOf(Looper .getMainLooper()).runToEndOfTasks()
375+
376+ verify(client).process(rawMessage)
377+ }
378+
379+ @Test
380+ fun `ec error with unrecoverable severity dismisses via listener` () {
381+ val rawMessage = ecErrorMessage(severity = " unrecoverable" )
382+ val client = mock<CheckoutCommunicationClient >()
383+ ecp.setClient(client)
384+
385+ ecp.postMessage(rawMessage)
386+ shadowOf(Looper .getMainLooper()).runToEndOfTasks()
387+
388+ verify(client).process(rawMessage)
389+ val captor = argumentCaptor<CheckoutException >()
390+ verify(mockListener).onCheckoutViewFailedWithError(captor.capture())
391+ assertThat(captor.firstValue).isInstanceOf(ClientException ::class .java)
392+ assertThat(captor.firstValue.errorDescription)
393+ .isEqualTo(" Embedded checkout reported unrecoverable error." )
394+ }
364395
365396 @Test
366- fun `ec error is delegated to client` () {
367- val rawMessage = """ {"jsonrpc":"2.0","method":"ec.error","params":{"error":{"code":-1,"message":"fail"}}} """
397+ fun `ec error with unrecoverable severity dismisses even when client returns response ` () {
398+ val rawMessage = ecErrorMessage(severity = " unrecoverable " )
368399 val client = mock<CheckoutCommunicationClient >()
400+ whenever(client.process(rawMessage)).thenReturn(""" {"jsonrpc":"2.0","id":null,"result":{}}""" )
369401 ecp.setClient(client)
370402
371403 ecp.postMessage(rawMessage)
372404 shadowOf(Looper .getMainLooper()).runToEndOfTasks()
373405
374406 verify(client).process(rawMessage)
407+ verify(mockListener).onCheckoutViewFailedWithError(
408+ argThat { this is ClientException },
409+ )
410+ }
411+
412+ @Test
413+ fun `ec error with recoverable severity does not dismiss` () {
414+ val rawMessage = ecErrorMessage(severity = " recoverable" )
415+ ecp.setClient(mock())
416+
417+ ecp.postMessage(rawMessage)
418+ shadowOf(Looper .getMainLooper()).runToEndOfTasks()
419+
420+ verify(mockListener, never()).onCheckoutViewFailedWithError(any())
375421 }
376422
423+ @Test
424+ fun `ec error with requires_buyer_input severity does not dismiss` () {
425+ val rawMessage = ecErrorMessage(severity = " requires_buyer_input" )
426+ ecp.setClient(mock())
427+
428+ ecp.postMessage(rawMessage)
429+ shadowOf(Looper .getMainLooper()).runToEndOfTasks()
430+
431+ verify(mockListener, never()).onCheckoutViewFailedWithError(any())
432+ }
433+
434+ @Test
435+ fun `ec error with requires_buyer_review severity does not dismiss` () {
436+ val rawMessage = ecErrorMessage(severity = " requires_buyer_review" )
437+ ecp.setClient(mock())
438+
439+ ecp.postMessage(rawMessage)
440+ shadowOf(Looper .getMainLooper()).runToEndOfTasks()
441+
442+ verify(mockListener, never()).onCheckoutViewFailedWithError(any())
443+ }
444+
445+ @Test
446+ fun `ec error dismisses when any message has unrecoverable severity` () {
447+ val messages = """ [
448+ |{"type":"error","code":"a","content":"x","severity":"recoverable"},
449+ |{"type":"error","code":"b","content":"y","severity":"unrecoverable"}
450+ |]
451+ """ .trimMargin()
452+ val rawMessage = """ {"jsonrpc":"2.0","method":"ec.error","params":{"messages":$messages }}"""
453+ ecp.setClient(mock())
454+
455+ ecp.postMessage(rawMessage)
456+ shadowOf(Looper .getMainLooper()).runToEndOfTasks()
457+
458+ verify(mockListener).onCheckoutViewFailedWithError(
459+ argThat { this is ClientException },
460+ )
461+ }
462+
463+ @Test
464+ fun `ec error without required messages field is ignored by typed handler` () {
465+ val rawMessage = """ {"jsonrpc":"2.0","method":"ec.error","params":{}}"""
466+ val client = CheckoutProtocol .Client ()
467+ .on(CheckoutProtocol .error) { fail(" Malformed ec.error should not dispatch" ) }
468+ ecp.setClient(client)
469+
470+ ecp.postMessage(rawMessage)
471+ shadowOf(Looper .getMainLooper()).runToEndOfTasks()
472+
473+ verify(mockListener, never()).onCheckoutViewFailedWithError(any())
474+ }
475+
476+ // endregion
477+
478+ // region delegated notifications
479+
377480 @Test
378481 fun `ec complete is delegated to client` () {
379482 val rawMessage = """ {"jsonrpc":"2.0","method":"ec.complete","params":{"checkout":{}}}"""
@@ -491,6 +594,12 @@ class EmbeddedCheckoutProtocolTest {
491594 private fun windowOpenRequest (id : String , url : String ): String =
492595 """ {"jsonrpc":"2.0","method":"ec.window.open_request","id":$id ,"params":{"url":"$url "}}"""
493596
597+ private fun ecErrorMessage (severity : String ): String {
598+ val messages =
599+ """ [{"type":"error","code":"session_failed","content":"Session failed","severity":"$severity "}]"""
600+ return """ {"jsonrpc":"2.0","method":"ec.error","params":{"messages":$messages }}"""
601+ }
602+
494603 /* *
495604 * Runs [block], drains the main-thread queue, captures the first JS string
496605 * passed to [CheckoutWebView.evaluateJavascript].
0 commit comments