1 /*
2  * Copyright (C) 2022 The Android Open Source Project
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.android.adservices.service.js;
18 
19 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
20 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
21 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
22 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 
26 import static org.junit.Assert.assertThrows;
27 
28 import android.content.Context;
29 
30 import androidx.javascriptengine.JavaScriptSandbox;
31 import androidx.test.core.app.ApplicationProvider;
32 import androidx.test.filters.SmallTest;
33 
34 import com.android.adservices.LoggerFactory;
35 import com.android.adservices.service.profiling.JSScriptEngineLogConstants;
36 import com.android.adservices.service.profiling.Profiler;
37 import com.android.adservices.service.profiling.StopWatch;
38 import com.android.adservices.shared.testing.FutureSyncCallback;
39 import com.android.adservices.shared.testing.SdkLevelSupportRule;
40 import com.android.dx.mockito.inline.extended.ExtendedMockito;
41 import com.android.dx.mockito.inline.extended.StaticMockitoSession;
42 
43 import com.google.common.util.concurrent.FluentFuture;
44 import com.google.common.util.concurrent.Futures;
45 
46 import org.junit.After;
47 import org.junit.Before;
48 import org.junit.Rule;
49 import org.junit.Test;
50 import org.junit.function.ThrowingRunnable;
51 import org.mockito.Mock;
52 import org.mockito.Mockito;
53 
54 import java.util.concurrent.ExecutionException;
55 import java.util.concurrent.TimeUnit;
56 import java.util.concurrent.TimeoutException;
57 
58 @SmallTest
59 public class JavaScriptSandboxProviderTest {
60     private final Context mApplicationContext = ApplicationProvider.getApplicationContext();
61     private final LoggerFactory.Logger mLogger = LoggerFactory.getFledgeLogger();
62     private StaticMockitoSession mStaticMockSession;
63     @Mock private StopWatch mSandboxInitWatch;
64     @Mock private JavaScriptSandbox mSandbox;
65     @Mock private JavaScriptSandbox mSandbox2;
66 
67     @Mock private Profiler mProfilerMock;
68 
69     private JSScriptEngine.JavaScriptSandboxProvider mJsSandboxProvider;
70 
71     @Rule(order = 0)
72     public final SdkLevelSupportRule sdkLevel = SdkLevelSupportRule.forAtLeastS();
73 
74     @Before
setUp()75     public void setUp() {
76         mStaticMockSession =
77                 ExtendedMockito.mockitoSession()
78                         .mockStatic(JavaScriptSandbox.class)
79                         .initMocks(this)
80                         .startMocking();
81     }
82 
83     @After
shutDown()84     public void shutDown() {
85         mStaticMockSession.finishMocking();
86     }
87 
88     @Test
testJsSandboxProviderCreateFailsIfSandboxNotSupported()89     public void testJsSandboxProviderCreateFailsIfSandboxNotSupported() {
90         when(JavaScriptSandbox.isSupported()).thenReturn(false);
91         mJsSandboxProvider = new JSScriptEngine.JavaScriptSandboxProvider(mProfilerMock, mLogger);
92         ThrowingRunnable getFutureInstance =
93                 () -> mJsSandboxProvider.getFutureInstance(mApplicationContext).get();
94         Exception futureException = assertThrows(ExecutionException.class, getFutureInstance);
95         assertThat(futureException)
96                 .hasCauseThat()
97                 .isInstanceOf(JSSandboxIsNotAvailableException.class);
98         verify(JavaScriptSandbox::isSupported);
99     }
100 
101     @Test
testJsSandboxProviderCreatesOnlyOneInstance()102     public void testJsSandboxProviderCreatesOnlyOneInstance()
103             throws ExecutionException, InterruptedException, TimeoutException {
104         when(JavaScriptSandbox.isSupported()).thenReturn(true);
105         doReturn(Futures.immediateFuture(mSandbox))
106                 .when(
107                         () -> {
108                             return JavaScriptSandbox.createConnectedInstanceAsync(
109                                     mApplicationContext);
110                         });
111 
112         when(mProfilerMock.start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME))
113                 .thenReturn(mSandboxInitWatch);
114         mJsSandboxProvider = new JSScriptEngine.JavaScriptSandboxProvider(mProfilerMock, mLogger);
115 
116         mJsSandboxProvider.getFutureInstance(mApplicationContext).get(5, TimeUnit.SECONDS);
117         mJsSandboxProvider.getFutureInstance(mApplicationContext).get(5, TimeUnit.SECONDS);
118 
119         verify(() -> JavaScriptSandbox.createConnectedInstanceAsync(mApplicationContext));
120         verify(mProfilerMock).start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME);
121     }
122 
123     @Test
testJsSandboxProviderCreatesNewInstanceAfterFirstIsDestroyed()124     public void testJsSandboxProviderCreatesNewInstanceAfterFirstIsDestroyed()
125             throws ExecutionException, InterruptedException, TimeoutException {
126         when(JavaScriptSandbox.isSupported()).thenReturn(true);
127         doReturn(Futures.immediateFuture(mSandbox))
128                 .when(
129                         () -> {
130                             return JavaScriptSandbox.createConnectedInstanceAsync(
131                                     mApplicationContext);
132                         });
133 
134         when(mProfilerMock.start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME))
135                 .thenReturn(mSandboxInitWatch);
136         mJsSandboxProvider = new JSScriptEngine.JavaScriptSandboxProvider(mProfilerMock, mLogger);
137         mJsSandboxProvider.getFutureInstance(mApplicationContext).get(5, TimeUnit.SECONDS);
138 
139         // Waiting for the first instance closure
140         mJsSandboxProvider.destroyCurrentInstance().get(4, TimeUnit.SECONDS);
141 
142         mJsSandboxProvider.getFutureInstance(mApplicationContext).get(5, TimeUnit.SECONDS);
143 
144         verify(
145                 () -> JavaScriptSandbox.createConnectedInstanceAsync(mApplicationContext),
146                 Mockito.times(2));
147 
148         verify(mProfilerMock, Mockito.times(2)).start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME);
149     }
150 
151     @Test
testJsSandboxProviderDestroysOnlyIfCurrentInstance()152     public void testJsSandboxProviderDestroysOnlyIfCurrentInstance()
153             throws ExecutionException, InterruptedException, TimeoutException {
154         when(JavaScriptSandbox.isSupported()).thenReturn(true);
155         when(JavaScriptSandbox.createConnectedInstanceAsync(mApplicationContext))
156                 .thenReturn(Futures.immediateFuture(mSandbox))
157                 .thenReturn(Futures.immediateFuture(mSandbox2));
158         doNothing().when(mSandbox).close();
159         when(mProfilerMock.start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME))
160                 .thenReturn(mSandboxInitWatch);
161 
162         mJsSandboxProvider = new JSScriptEngine.JavaScriptSandboxProvider(mProfilerMock, mLogger);
163         JavaScriptSandbox sandbox1 =
164                 mJsSandboxProvider.getFutureInstance(mApplicationContext).get(5, TimeUnit.SECONDS);
165         // Waiting for the first instance closure
166         mJsSandboxProvider.destroyIfCurrentInstance(sandbox1).get(4, TimeUnit.SECONDS);
167         mJsSandboxProvider.getFutureInstance(mApplicationContext).get(5, TimeUnit.SECONDS);
168         mJsSandboxProvider.destroyIfCurrentInstance(sandbox1).get(4, TimeUnit.SECONDS);
169 
170         verify(mSandbox).close();
171         verify(mSandbox2, Mockito.never()).close();
172         verify(mProfilerMock, Mockito.times(2)).start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME);
173     }
174 
175     @Test
testJsSandboxProviderDestroysOnlyIfCurrentInstanceOnlyOnce()176     public void testJsSandboxProviderDestroysOnlyIfCurrentInstanceOnlyOnce()
177             throws ExecutionException, InterruptedException, TimeoutException {
178         when(JavaScriptSandbox.isSupported()).thenReturn(true);
179         when(JavaScriptSandbox.createConnectedInstanceAsync(mApplicationContext))
180                 .thenReturn(Futures.immediateFuture(mSandbox))
181                 .thenThrow(
182                         new IllegalStateException(
183                                 "createConnectedInstanceAsync should only be called once from the"
184                                         + " test"));
185         doNothing().when(mSandbox).close();
186         when(mProfilerMock.start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME))
187                 .thenReturn(mSandboxInitWatch);
188 
189         mJsSandboxProvider = new JSScriptEngine.JavaScriptSandboxProvider(mProfilerMock, mLogger);
190         JavaScriptSandbox sandbox1 =
191                 mJsSandboxProvider.getFutureInstance(mApplicationContext).get(5, TimeUnit.SECONDS);
192         FutureSyncCallback<Void> callback1 = new FutureSyncCallback<>();
193         FutureSyncCallback<Void> callback2 = new FutureSyncCallback<>();
194         FluentFuture.from(mJsSandboxProvider.destroyIfCurrentInstance(sandbox1))
195                 .addCallback(callback1, Runnable::run);
196         FluentFuture.from(mJsSandboxProvider.destroyIfCurrentInstance(sandbox1))
197                 .addCallback(callback2, Runnable::run);
198 
199         callback1.assertResultReceived();
200         callback2.assertResultReceived();
201         verify(mSandbox).close();
202     }
203 }
204