• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 Google LLC
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.google.android.libraries.mobiledatadownload.file;
17 
18 import static com.google.android.libraries.mobiledatadownload.file.common.testing.FragmentParamMatchers.eqParam;
19 import static com.google.common.truth.Truth.assertThat;
20 import static org.junit.Assert.assertThrows;
21 import static org.mockito.ArgumentMatchers.any;
22 import static org.mockito.Mockito.atLeast;
23 import static org.mockito.Mockito.doThrow;
24 import static org.mockito.Mockito.eq;
25 import static org.mockito.Mockito.inOrder;
26 import static org.mockito.Mockito.never;
27 import static org.mockito.Mockito.verify;
28 import static org.mockito.Mockito.verifyNoMoreInteractions;
29 import static org.mockito.Mockito.when;
30 
31 import android.content.Context;
32 import android.net.Uri;
33 import androidx.test.core.app.ApplicationProvider;
34 import com.google.android.libraries.mobiledatadownload.file.backends.AndroidFileBackend;
35 import com.google.android.libraries.mobiledatadownload.file.backends.JavaFileBackend;
36 import com.google.android.libraries.mobiledatadownload.file.common.GcParam;
37 import com.google.android.libraries.mobiledatadownload.file.common.UnsupportedFileStorageOperation;
38 import com.google.android.libraries.mobiledatadownload.file.common.testing.BufferingMonitor;
39 import com.google.android.libraries.mobiledatadownload.file.common.testing.FileStorageTestBase;
40 import com.google.android.libraries.mobiledatadownload.file.common.testing.NoOpMonitor;
41 import com.google.android.libraries.mobiledatadownload.file.openers.AppendStreamOpener;
42 import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener;
43 import com.google.android.libraries.mobiledatadownload.file.openers.WriteStreamOpener;
44 import com.google.android.libraries.mobiledatadownload.file.spi.Backend;
45 import com.google.android.libraries.mobiledatadownload.file.spi.ForwardingBackend;
46 import com.google.android.libraries.mobiledatadownload.file.spi.Transform;
47 import com.google.android.libraries.mobiledatadownload.file.transforms.BufferTransform;
48 import com.google.android.libraries.mobiledatadownload.file.transforms.CompressTransform;
49 import com.google.common.collect.ImmutableList;
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.io.OutputStream;
53 import java.util.Arrays;
54 import java.util.Collections;
55 import java.util.Date;
56 import org.junit.Test;
57 import org.junit.runner.RunWith;
58 import org.mockito.InOrder;
59 import org.robolectric.RobolectricTestRunner;
60 
61 /**
62  * Test {@link com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage}. These
63  * tests use mocks and basically just ensure that things are being called in the expected order.
64  */
65 @RunWith(RobolectricTestRunner.class)
66 public class SynchronousFileStorageTest extends FileStorageTestBase {
67 
68   private SynchronousFileStorage storage;
69   private final Context context = ApplicationProvider.getApplicationContext();
70 
71   @Override
initStorage()72   protected void initStorage() {
73     storage =
74         new SynchronousFileStorage(
75             ImmutableList.of(fileBackend, cnsBackend),
76             ImmutableList.of(compressTransform, encryptTransform, identityTransform),
77             ImmutableList.of(countingMonitor));
78   }
79 
80   // Backend registrar
81 
82   @Test
registeredBackends_shouldNotThrowException()83   public void registeredBackends_shouldNotThrowException() throws Exception {
84     assertThat(storage.exists(file1Uri)).isFalse();
85   }
86 
87   @Test
unregisteredBackends_shouldThrowException()88   public void unregisteredBackends_shouldThrowException() throws Exception {
89     Uri unregisteredUri = Uri.parse("unregistered:///");
90     assertThrows(UnsupportedFileStorageOperation.class, () -> storage.exists(unregisteredUri));
91   }
92 
93   @Test
nullUriScheme_shouldThrowException()94   public void nullUriScheme_shouldThrowException() throws Exception {
95     Uri relativeUri = Uri.parse("/relative/uri");
96     assertThrows(UnsupportedFileStorageOperation.class, () -> storage.exists(relativeUri));
97   }
98 
99   @Test
emptyBackendName_shouldBeSilentlySkipped()100   public void emptyBackendName_shouldBeSilentlySkipped() throws Exception {
101     Backend emptyNameBackend =
102         new ForwardingBackend() {
103           @Override
104           protected Backend delegate() {
105             return fileBackend;
106           }
107 
108           @Override
109           public String name() {
110             return "";
111           }
112         };
113     var unused = new SynchronousFileStorage(ImmutableList.of(emptyNameBackend));
114   }
115 
116   @Test
doubleRegisteringBackendName_shouldThrowException()117   public void doubleRegisteringBackendName_shouldThrowException() throws Exception {
118     assertThrows(
119         IllegalArgumentException.class,
120         () ->
121             new SynchronousFileStorage(
122                 ImmutableList.of(new JavaFileBackend(), new JavaFileBackend())));
123   }
124 
125   // Backend operations
126 
127   @Test
deleteFile_shouldInvokeBackend()128   public void deleteFile_shouldInvokeBackend() throws Exception {
129     storage.deleteFile(file1Uri);
130     verify(fileBackend).deleteFile(file1Uri);
131   }
132 
133   @Test
deleteDir_shouldInvokeBackend()134   public void deleteDir_shouldInvokeBackend() throws Exception {
135     storage.deleteDirectory(file1Uri);
136     verify(fileBackend).deleteDirectory(file1Uri);
137   }
138 
139   @Test
deleteRecursively_shouldRecurse()140   public void deleteRecursively_shouldRecurse() throws Exception {
141     Uri dir2Uri = dir1Uri.buildUpon().appendPath("dir2").build();
142     when(fileBackend.exists(dir1Uri)).thenReturn(true);
143     when(fileBackend.isDirectory(dir1Uri)).thenReturn(true);
144     when(fileBackend.exists(dir2Uri)).thenReturn(true);
145     when(fileBackend.isDirectory(dir2Uri)).thenReturn(true);
146     when(fileBackend.exists(file1Uri)).thenReturn(true);
147     when(fileBackend.exists(file2Uri)).thenReturn(true);
148     when(fileBackend.children(dir1Uri)).thenReturn(ImmutableList.of(file1Uri, file2Uri, dir2Uri));
149     when(fileBackend.children(dir2Uri)).thenReturn(Collections.emptyList());
150 
151     assertThat(storage.deleteRecursively(dir1Uri)).isTrue();
152 
153     verify(fileBackend).deleteFile(file1Uri);
154     verify(fileBackend).deleteFile(file2Uri);
155     verify(fileBackend).deleteDirectory(dir2Uri);
156     verify(fileBackend).deleteDirectory(dir1Uri);
157   }
158 
159   @Test
deleteRecursively_failsOnAccessError()160   public void deleteRecursively_failsOnAccessError() throws Exception {
161     when(fileBackend.exists(dir1Uri)).thenReturn(true);
162     when(fileBackend.isDirectory(dir1Uri)).thenReturn(true);
163     when(fileBackend.exists(file1Uri)).thenReturn(true);
164     when(fileBackend.exists(file2Uri)).thenReturn(true);
165     when(fileBackend.children(dir1Uri)).thenReturn(ImmutableList.of(file1Uri, file2Uri));
166     doThrow(IOException.class).when(fileBackend).deleteFile(file2Uri);
167 
168     assertThrows(IOException.class, () -> storage.deleteRecursively(dir1Uri));
169 
170     verify(fileBackend).deleteFile(file1Uri);
171     verify(fileBackend).deleteFile(file2Uri);
172     verify(fileBackend, never()).deleteDirectory(dir1Uri);
173   }
174 
175   @Test
deleteRecursively_fileDeletes()176   public void deleteRecursively_fileDeletes() throws Exception {
177     when(fileBackend.exists(file1Uri)).thenReturn(true);
178     when(fileBackend.isDirectory(file1Uri)).thenReturn(false);
179 
180     assertThat(storage.deleteRecursively(file1Uri)).isTrue();
181 
182     verify(fileBackend).exists(file1Uri);
183     verify(fileBackend).deleteFile(file1Uri);
184   }
185 
186   @Test
deleteRecursively_fileNotExist()187   public void deleteRecursively_fileNotExist() throws Exception {
188     when(fileBackend.exists(dir1Uri)).thenReturn(false);
189 
190     assertThat(storage.deleteRecursively(dir1Uri)).isFalse();
191 
192     verify(fileBackend).exists(dir1Uri);
193   }
194 
195   @Test
rename_shouldInvokeBackend()196   public void rename_shouldInvokeBackend() throws Exception {
197     storage.rename(file1Uri, file2Uri);
198     verify(fileBackend).rename(file1Uri, file2Uri);
199   }
200 
201   @Test
rename_crossingBackendsShouldThrowException()202   public void rename_crossingBackendsShouldThrowException() throws Exception {
203     assertThrows(UnsupportedFileStorageOperation.class, () -> storage.rename(file1Uri, cnsUri));
204   }
205 
206   @Test
exists_shouldInvokeBackend()207   public void exists_shouldInvokeBackend() throws Exception {
208     assertThat(storage.exists(file1Uri)).isFalse();
209     verify(fileBackend).exists(file1Uri);
210   }
211 
212   @Test
isDirectory_shouldInvokeBackend()213   public void isDirectory_shouldInvokeBackend() throws Exception {
214     assertThat(storage.isDirectory(file1Uri)).isFalse();
215     verify(fileBackend).isDirectory(file1Uri);
216   }
217 
218   @Test
createDirectoryshouldInvokeBackend()219   public void createDirectoryshouldInvokeBackend() throws Exception {
220     storage.createDirectory(file1Uri);
221     verify(fileBackend).createDirectory(file1Uri);
222   }
223 
224   @Test
fileSize_shouldInvokeBackend()225   public void fileSize_shouldInvokeBackend() throws Exception {
226     assertThat(storage.fileSize(file1Uri)).isEqualTo(0L);
227     verify(fileBackend).fileSize(file1Uri);
228   }
229 
230   //
231   // Transform stuff
232   //
233 
234   @Test
registeredTransforms_shouldNotThrowException()235   public void registeredTransforms_shouldNotThrowException() throws Exception {
236     assertThat(storage.exists(file1CompressUri)).isFalse();
237     verify(fileBackend).exists(file1Uri);
238   }
239 
240   @Test
unregisteredTransforms_shouldThrowException()241   public void unregisteredTransforms_shouldThrowException() throws Exception {
242     Uri unregisteredUri = Uri.parse(file1Uri + "#transform=unregistered");
243     assertThrows(UnsupportedFileStorageOperation.class, () -> storage.exists(unregisteredUri));
244   }
245 
246   @Test
getDebugInfo_shouldIncludeRegisteredPlugins()247   public void getDebugInfo_shouldIncludeRegisteredPlugins() throws Exception {
248     SynchronousFileStorage debugStorage =
249         new SynchronousFileStorage(
250             ImmutableList.of(new JavaFileBackend(), AndroidFileBackend.builder(context).build()),
251             ImmutableList.of(new CompressTransform(), new BufferTransform()),
252             ImmutableList.of(new BufferingMonitor(), new NoOpMonitor()));
253     String debugString = debugStorage.getDebugInfo();
254 
255     assertThat(debugString)
256         .isEqualTo(
257             "Registered Mobstore Plugins:\n"
258                 + "\n"
259                 + "Backends:\n"
260                 + "protocol: android, class: AndroidFileBackend,\n"
261                 + "protocol: file, class: JavaFileBackend\n"
262                 + "\n"
263                 + "Transforms:\n"
264                 + "BufferTransform,\n"
265                 + "CompressTransform\n"
266                 + "\n"
267                 + "Monitors:\n"
268                 + "BufferingMonitor,\n"
269                 + "NoOpMonitor");
270   }
271 
272   @Test
emptyTransformName_shouldBeSilentlySkipped()273   public void emptyTransformName_shouldBeSilentlySkipped() throws Exception {
274     Transform emptyNameTransform =
275         new Transform() {
276           @Override
277           public String name() {
278             return "";
279           }
280         };
281     var unused =
282         new SynchronousFileStorage(ImmutableList.of(), ImmutableList.of(emptyNameTransform));
283   }
284 
285   @Test
doubleRegisteringTransformName_shouldThrowException()286   public void doubleRegisteringTransformName_shouldThrowException() throws Exception {
287     assertThrows(
288         IllegalArgumentException.class,
289         () ->
290             new SynchronousFileStorage(
291                 ImmutableList.of(),
292                 ImmutableList.of(new CompressTransform(), new CompressTransform())));
293   }
294 
295   @Test
read_shouldInvokeTransforms()296   public void read_shouldInvokeTransforms() throws Exception {
297     when(compressTransform.wrapForRead(eqParam(uriWithCompressParam), any(InputStream.class)))
298         .thenReturn(compressInputStream);
299     try (InputStream in = storage.open(file1CompressUri, ReadStreamOpener.create())) {
300       verify(compressTransform).wrapForRead(eqParam(uriWithCompressParam), any(InputStream.class));
301       verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename));
302     }
303   }
304 
305   @Test
read_shouldInvokeTransformsWithEncoded()306   public void read_shouldInvokeTransformsWithEncoded() throws Exception {
307     when(compressTransform.wrapForRead(
308             eqParam(uriWithCompressParamWithEncoded), any(InputStream.class)))
309         .thenReturn(compressInputStream);
310     try (InputStream in = storage.open(file1CompressUriWithEncoded, ReadStreamOpener.create())) {
311       verify(compressTransform)
312           .wrapForRead(eqParam(uriWithCompressParamWithEncoded), any(InputStream.class));
313       verify(compressTransform).encode(eqParam(uriWithCompressParamWithEncoded), eq(file1Filename));
314     }
315   }
316 
317   @Test
write_shouldInvokeTransforms()318   public void write_shouldInvokeTransforms() throws Exception {
319     when(compressTransform.wrapForWrite(eqParam(uriWithCompressParam), any(OutputStream.class)))
320         .thenReturn(compressOutputStream);
321     try (OutputStream out = storage.open(file1CompressUri, WriteStreamOpener.create())) {
322       verify(compressTransform)
323           .wrapForWrite(eqParam(uriWithCompressParam), any(OutputStream.class));
324       verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename));
325     }
326   }
327 
328   @Test
deleteFile_shouldInvokeTransformEncode()329   public void deleteFile_shouldInvokeTransformEncode() throws Exception {
330     storage.deleteFile(file1CompressUri);
331     verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename));
332   }
333 
334   @Test
deleteDirectory_shouldNOTInvokeTransformEncode()335   public void deleteDirectory_shouldNOTInvokeTransformEncode() throws Exception {
336     storage.deleteDirectory(file1CompressUri);
337     verify(compressTransform, never()).encode(eqParam(uriWithCompressParam), any());
338   }
339 
340   @Test
rename_shouldInvokeTransformEncode()341   public void rename_shouldInvokeTransformEncode() throws Exception {
342     storage.rename(file1CompressUri, file2CompressEncryptUri);
343     verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename));
344     verify(encryptTransform).encode(eqParam(uriWithEncryptParam), eq(file2Filename));
345     verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file2Filename));
346   }
347 
348   @Test
rename_shouldNOTInvokeTransformEncodeOnDirectory()349   public void rename_shouldNOTInvokeTransformEncodeOnDirectory() throws Exception {
350     storage.rename(dir1Uri, dir2CompressUri);
351     verify(compressTransform, never()).encode(eqParam(uriWithCompressParam), any());
352   }
353 
354   @Test
exists_shouldInvokeTransformEncode()355   public void exists_shouldInvokeTransformEncode() throws Exception {
356     assertThat(storage.exists(file1CompressUri)).isFalse();
357     verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename));
358   }
359 
360   @Test
exists_shouldNOTInvokeTransformEncodeOnDirectory()361   public void exists_shouldNOTInvokeTransformEncodeOnDirectory() throws Exception {
362     assertThat(storage.exists(dir2CompressUri)).isFalse();
363     verify(compressTransform, never()).encode(eqParam(uriWithCompressParam), any());
364   }
365 
366   @Test
isDirectory_shouldNOTInvokeTransformEncode()367   public void isDirectory_shouldNOTInvokeTransformEncode() throws Exception {
368     assertThat(storage.isDirectory(file1CompressUri)).isFalse();
369     verify(compressTransform, never()).encode(eqParam(uriWithCompressParam), any());
370   }
371 
372   @Test
createDirectoryshouldNOTInvokeTransformEncode()373   public void createDirectoryshouldNOTInvokeTransformEncode() throws Exception {
374     storage.createDirectory(file1CompressUri);
375     verify(compressTransform, never()).encode(eqParam(uriWithCompressParam), any());
376   }
377 
378   @Test
fileSize_shouldInvokeTransformEncode()379   public void fileSize_shouldInvokeTransformEncode() throws Exception {
380     assertThat(storage.fileSize(file1CompressUri)).isEqualTo(0L);
381     verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename));
382   }
383 
384   @Test
multipleTransformsShouldBeEncodedForwardAndComposedInReverse()385   public void multipleTransformsShouldBeEncodedForwardAndComposedInReverse() throws Exception {
386     // The spec "transform=compress+encrypt" means the data is compressed and then
387     // encrypted before stored. Since transforms are implemented by wrapping transforms,
388     // they need to be instantiated in the reverse order. So, in this case,
389     // 1. encrypt is instantiated
390     // 2. encrypt wraps the backend stream
391     // 3. compress is instantiated
392     // 4. compress wraps the encrypted stream
393     // 5. the compress transforms stream is returned to the client
394     // In contrast, encode() is called in the order in which transforms appear in the fragment.
395 
396     when(encryptTransform.wrapForWrite(eqParam(uriWithEncryptParam), any(OutputStream.class)))
397         .thenReturn(encryptOutputStream);
398     when(compressTransform.wrapForWrite(eqParam(uriWithCompressParam), eq(encryptOutputStream)))
399         .thenReturn(compressOutputStream);
400     try (OutputStream out = storage.open(file2CompressEncryptUri, WriteStreamOpener.create())) {
401 
402       InOrder forward = inOrder(compressTransform, encryptTransform);
403       forward.verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file2Filename));
404       forward.verify(encryptTransform).encode(eqParam(uriWithEncryptParam), eq(file2Filename));
405 
406       InOrder reverse = inOrder(encryptTransform, compressTransform);
407       reverse
408           .verify(encryptTransform)
409           .wrapForWrite(eqParam(uriWithEncryptParam), any(OutputStream.class));
410       reverse
411           .verify(compressTransform)
412           .wrapForWrite(eqParam(uriWithCompressParam), eq(encryptOutputStream));
413     }
414   }
415 
416   @Test
children_shouldInvokeTransformDecodeInReverse()417   public void children_shouldInvokeTransformDecodeInReverse() throws Exception {
418     // The spec "transform=compress+encrypt" means the data is compressed and then encrypted.
419     // When listing children, transform decodes() are invoked in reverse.
420 
421     when(fileBackend.children(eq(file2Uri))).thenReturn(Arrays.asList(Uri.parse("file:///child1")));
422     assertThat(storage.children(file2CompressEncryptUri)).isNotNull();
423 
424     InOrder reverse = inOrder(encryptTransform, compressTransform);
425     reverse.verify(encryptTransform).decode(eqParam(uriWithEncryptParam), eq("child1"));
426     reverse.verify(compressTransform).decode(eqParam(uriWithCompressParam), eq("child1"));
427   }
428 
429   @Test
children_transformsShouldNotDecodeSubdirectories()430   public void children_transformsShouldNotDecodeSubdirectories() throws Exception {
431     when(fileBackend.children(eq(file1Uri)))
432         .thenReturn(
433             Arrays.asList(
434                 Uri.parse("file:///file1"),
435                 Uri.parse("file:///file2"),
436                 Uri.parse("file:///dir1/")));
437     assertThat(storage.children(file1CompressUri)).isNotNull();
438 
439     verify(compressTransform).decode(eqParam(uriWithCompressParam), eq("file1"));
440     verify(compressTransform).decode(eqParam(uriWithCompressParam), eq("file2"));
441     verify(compressTransform, never()).decode(eqParam(uriWithCompressParam), eq("dir1"));
442     verify(compressTransform, atLeast(1)).name();
443     verifyNoMoreInteractions(compressTransform);
444   }
445 
446   //
447   // Monitor stuff
448   //
449 
450   @Test
read_shouldMonitor()451   public void read_shouldMonitor() throws Exception {
452     try (InputStream in = storage.open(file1Uri, ReadStreamOpener.create())) {
453       verify(countingMonitor).monitorRead(file1Uri);
454     }
455   }
456 
457   @Test
write_shouldMonitor()458   public void write_shouldMonitor() throws Exception {
459     try (OutputStream out = storage.open(file1Uri, WriteStreamOpener.create())) {
460       verify(countingMonitor).monitorWrite(file1Uri);
461     }
462   }
463 
464   @Test
append_shouldMonitor()465   public void append_shouldMonitor() throws Exception {
466     try (OutputStream out = storage.open(file1Uri, AppendStreamOpener.create())) {
467       verify(countingMonitor).monitorAppend(file1Uri);
468     }
469   }
470 
471   @Test
readWithTransform_shouldGetOriginalUri()472   public void readWithTransform_shouldGetOriginalUri() throws Exception {
473     try (InputStream in = storage.open(file1CompressUri, ReadStreamOpener.create())) {
474       verify(countingMonitor).monitorRead(file1CompressUri);
475     }
476   }
477 
478   //
479   // MobStoreGc stuff
480   //
481 
482   @Test
gcMethods_shouldInvokeCorrespondingBackendMethods()483   public void gcMethods_shouldInvokeCorrespondingBackendMethods() throws Exception {
484     GcParam param = GcParam.expiresAt(new Date(1L));
485     storage.setGcParam(file1Uri, param);
486     verify(fileBackend).setGcParam(eq(file1Uri), eq(param));
487     storage.getGcParam(file1Uri);
488     verify(fileBackend).getGcParam(eq(file1Uri));
489   }
490 }
491