1 /*
2  * Copyright (C) 2020 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 package com.android.csuite.config;
17 
18 import static com.android.csuite.testing.Correspondences.instanceOf;
19 import static com.android.csuite.testing.MoreAsserts.assertThrows;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import static java.nio.charset.StandardCharsets.UTF_8;
24 
25 import com.android.tradefed.build.BuildRetrievalError;
26 import com.android.tradefed.config.ConfigurationException;
27 import com.android.tradefed.config.Option;
28 import com.android.tradefed.config.OptionSetter;
29 import com.android.tradefed.config.remote.IRemoteFileResolver;
30 
31 import com.google.common.collect.ImmutableList;
32 import com.google.common.collect.ImmutableMap;
33 import com.google.common.testing.NullPointerTester;
34 
35 import org.junit.Rule;
36 import org.junit.Test;
37 import org.junit.rules.TemporaryFolder;
38 import org.junit.runner.RunWith;
39 import org.junit.runners.JUnit4;
40 
41 import java.io.File;
42 import java.io.FileOutputStream;
43 import java.io.IOException;
44 import java.io.OutputStreamWriter;
45 import java.io.PrintWriter;
46 import java.net.URL;
47 import java.net.URLClassLoader;
48 import java.util.ServiceLoader;
49 import java.util.jar.JarEntry;
50 import java.util.jar.JarOutputStream;
51 
52 @RunWith(JUnit4.class)
53 public final class AppRemoteFileResolverTest {
54 
55     private static final String PACKAGE_NAME = "com.example.app";
56     private static final File APP_URI_FILE = uriToFile("app://" + PACKAGE_NAME);
57     private static final ImmutableMap<String, String> EMPTY_PARAMS = ImmutableMap.of();
58 
59     @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
60 
61     // Class sanity tests.
62 
63     @Test
isServiceLoadable()64     public void isServiceLoadable() throws Exception {
65         ClassLoader classLoader = classLoaderWithProviders(AppRemoteFileResolver.class.getName());
66 
67         ServiceLoader<IRemoteFileResolver> serviceLoader =
68                 ServiceLoader.load(IRemoteFileResolver.class, classLoader);
69 
70         // Copy the list to provide better failure error messages since ServiceLoader's string
71         // representation is not very informative.
72         assertThat(ImmutableList.copyOf(serviceLoader))
73                 .comparingElementsUsing(instanceOf())
74                 .contains(AppRemoteFileResolver.class);
75     }
76 
77     @Test
nullPointers()78     public void nullPointers() {
79         NullPointerTester tester = new NullPointerTester();
80         tester.setDefault(File.class, APP_URI_FILE);
81         tester.testAllPublicConstructors(AppRemoteFileResolver.class);
82         tester.testAllPublicInstanceMethods(new AppRemoteFileResolver());
83     }
84 
85     // URI validation tests.
86 
87     @Test
unsupportedUriScheme_throwsException()88     public void unsupportedUriScheme_throwsException() throws Exception {
89         AppRemoteFileResolver resolver = newResolverWithAnyTemplate();
90         String uri = "gs://" + PACKAGE_NAME;
91         File f = uriToFile(uri);
92 
93         Throwable thrown =
94                 assertThrows(
95                         IllegalArgumentException.class,
96                         () -> resolver.resolveRemoteFiles(f, EMPTY_PARAMS));
97 
98         assertThat(thrown).hasMessageThat().contains("(gs)");
99         assertThat(thrown).hasMessageThat().contains(uri);
100     }
101 
102     @Test
opaqueUri_throwsException()103     public void opaqueUri_throwsException() throws Exception {
104         AppRemoteFileResolver resolver = newResolverWithAnyTemplate();
105         File uri = uriToFile("app:" + PACKAGE_NAME);
106 
107         Throwable thrown =
108                 assertThrows(
109                         IllegalArgumentException.class,
110                         () -> resolver.resolveRemoteFiles(uri, EMPTY_PARAMS));
111 
112         assertThat(thrown).hasMessageThat().contains("package name");
113     }
114 
115     @Test
uriHasPathComponent_throwsException()116     public void uriHasPathComponent_throwsException() throws Exception {
117         AppRemoteFileResolver resolver = newResolverWithAnyTemplate();
118         File uri = uriToFile("app://" + PACKAGE_NAME + "/invalid");
119 
120         Throwable thrown =
121                 assertThrows(
122                         IllegalArgumentException.class,
123                         () -> resolver.resolveRemoteFiles(uri, EMPTY_PARAMS));
124 
125         assertThat(thrown).hasMessageThat().contains("invalid");
126     }
127 
128     // Template validation and expansion tests.
129 
130     @Test
templateNotSet_returnsNull()131     public void templateNotSet_returnsNull() throws Exception {
132         AppRemoteFileResolver resolver = new AppRemoteFileResolver();
133 
134         File actual = resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS);
135 
136         assertThat(actual).isNull();
137     }
138 
139     @Test
emptyTemplate_throwsException()140     public void emptyTemplate_throwsException() throws Exception {
141         AppRemoteFileResolver resolver = newResolverWithTemplate("");
142 
143         Throwable thrown =
144                 assertThrows(
145                         IllegalStateException.class,
146                         () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS));
147 
148         assertThat(thrown).hasMessageThat().contains(AppRemoteFileResolver.URI_TEMPLATE_OPTION);
149     }
150 
151     @Test
templateHasNoPlaceholders_returnsFileWithoutExpansion()152     public void templateHasNoPlaceholders_returnsFileWithoutExpansion() throws Exception {
153         File expected = temporaryFolder.newFolder();
154         AppRemoteFileResolver resolver = newResolverWithTemplate(expected.toURI().toString());
155 
156         File actual = resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS);
157 
158         assertThat(actual).isEqualTo(expected);
159     }
160 
161     @Test
templateContainsPlaceholderForUndefinedVar_throwsException()162     public void templateContainsPlaceholderForUndefinedVar_throwsException() throws Exception {
163         AppRemoteFileResolver resolver = newResolverWithTemplate("file://{undefined}");
164 
165         Throwable thrown =
166                 assertThrows(
167                         IllegalStateException.class,
168                         () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS));
169 
170         assertThat(thrown).hasMessageThat().contains("undefined");
171     }
172 
173     @Test
templateExpandsToInvalidUri_throwsException()174     public void templateExpandsToInvalidUri_throwsException() throws Exception {
175         AppRemoteFileResolver resolver = newResolverWithTemplate("file:\\{package}");
176 
177         Throwable thrown =
178                 assertThrows(
179                         IllegalStateException.class,
180                         () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS));
181 
182         assertThat(thrown).hasMessageThat().contains(AppRemoteFileResolver.URI_TEMPLATE_OPTION);
183     }
184 
185     @Test
templateContainsPlaceholder_resolvesUriToFile()186     public void templateContainsPlaceholder_resolvesUriToFile() throws Exception {
187         File parent = temporaryFolder.newFolder();
188         File expected = new File(parent, PACKAGE_NAME);
189         String template = new File(parent, "{package}").toString();
190         AppRemoteFileResolver resolver = newResolverWithTemplate(template);
191         File uri = uriToFile("app://" + PACKAGE_NAME);
192 
193         File actual = resolver.resolveRemoteFiles(uri, EMPTY_PARAMS);
194 
195         assertThat(actual).isEqualTo(expected);
196     }
197 
198     @Test
templateExpandsToAppUri_throwsException()199     public void templateExpandsToAppUri_throwsException() throws Exception {
200         AppRemoteFileResolver resolver = newResolverWithTemplate("app://{package}");
201 
202         Throwable thrown =
203                 assertThrows(
204                         BuildRetrievalError.class,
205                         () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS));
206 
207         assertThat(thrown).hasMessageThat().contains("'app'");
208     }
209 
210     @Test
templateExpandsToUriWithUnsupportedScheme_returnsExpandedUri()211     public void templateExpandsToUriWithUnsupportedScheme_returnsExpandedUri() throws Exception {
212         String uri = "unsupported://" + PACKAGE_NAME;
213         AppRemoteFileResolver resolver = newResolverWithTemplate(uri);
214         File expected = uriToFile(uri);
215 
216         File actual = resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS);
217 
218         assertThat(actual).isEqualTo(expected);
219     }
220 
221     // Utility classes and methods.
222 
223     /**
224      * Constructs a File from a URI string using the same logic TradeFed uses since it's tricky and
225      * has some gotchas such as stripping slashes.
226      */
uriToFile(String str)227     private static File uriToFile(String str) {
228         FileOptionSource optionSource = new FileOptionSource();
229 
230         try {
231             OptionSetter setter = new OptionSetter(optionSource);
232             setter.setOptionValue(FileOptionSource.OPTION_NAME, str);
233         } catch (ConfigurationException e) {
234             throw new RuntimeException(e);
235         }
236 
237         return optionSource.file;
238     }
239 
240     private static final class FileOptionSource {
241         static final String OPTION_NAME = "file";
242 
243         @Option(name = OPTION_NAME)
244         public File file;
245     }
246 
newResolverWithAnyTemplate()247     private static AppRemoteFileResolver newResolverWithAnyTemplate()
248             throws ConfigurationException {
249         return newResolverWithTemplate("file:///tmp/{package}");
250     }
251 
newResolverWithTemplate(String uriTemplate)252     private static AppRemoteFileResolver newResolverWithTemplate(String uriTemplate)
253             throws ConfigurationException {
254         AppRemoteFileResolver resolver = new AppRemoteFileResolver();
255         OptionSetter setter = new OptionSetter(resolver);
256         setter.setOptionValue("app:" + AppRemoteFileResolver.URI_TEMPLATE_OPTION, uriTemplate);
257         return resolver;
258     }
259 
classLoaderWithProviders(String... lines)260     private ClassLoader classLoaderWithProviders(String... lines) throws IOException {
261         String service = IRemoteFileResolver.class.getName();
262         File jar = temporaryFolder.newFile();
263 
264         try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar))) {
265             JarEntry jarEntry = new JarEntry("META-INF/services/" + service);
266 
267             out.putNextEntry(jarEntry);
268             PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, UTF_8));
269 
270             for (String line : lines) {
271                 writer.println(line);
272             }
273 
274             writer.flush();
275         }
276 
277         return new URLClassLoader(new URL[] {jar.toURI().toURL()});
278     }
279 }
280