| /* |
| * Copyright 2022 Google LLC |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.google.android.libraries.mobiledatadownload.file; |
| |
| import static com.google.android.libraries.mobiledatadownload.file.common.testing.FragmentParamMatchers.eqParam; |
| import static com.google.common.truth.Truth.assertThat; |
| import static org.junit.Assert.assertThrows; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.Mockito.atLeast; |
| import static org.mockito.Mockito.doThrow; |
| import static org.mockito.Mockito.eq; |
| import static org.mockito.Mockito.inOrder; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.verifyNoMoreInteractions; |
| import static org.mockito.Mockito.when; |
| |
| import android.content.Context; |
| import android.net.Uri; |
| import androidx.test.core.app.ApplicationProvider; |
| import com.google.android.libraries.mobiledatadownload.file.backends.AndroidFileBackend; |
| import com.google.android.libraries.mobiledatadownload.file.backends.JavaFileBackend; |
| import com.google.android.libraries.mobiledatadownload.file.common.GcParam; |
| import com.google.android.libraries.mobiledatadownload.file.common.UnsupportedFileStorageOperation; |
| import com.google.android.libraries.mobiledatadownload.file.common.testing.BufferingMonitor; |
| import com.google.android.libraries.mobiledatadownload.file.common.testing.FileStorageTestBase; |
| import com.google.android.libraries.mobiledatadownload.file.common.testing.NoOpMonitor; |
| import com.google.android.libraries.mobiledatadownload.file.openers.AppendStreamOpener; |
| import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener; |
| import com.google.android.libraries.mobiledatadownload.file.openers.WriteStreamOpener; |
| import com.google.android.libraries.mobiledatadownload.file.spi.Backend; |
| import com.google.android.libraries.mobiledatadownload.file.spi.ForwardingBackend; |
| import com.google.android.libraries.mobiledatadownload.file.spi.Transform; |
| import com.google.android.libraries.mobiledatadownload.file.transforms.BufferTransform; |
| import com.google.android.libraries.mobiledatadownload.file.transforms.CompressTransform; |
| import com.google.common.collect.ImmutableList; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Date; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.InOrder; |
| import org.robolectric.RobolectricTestRunner; |
| |
| /** |
| * Test {@link com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage}. These |
| * tests use mocks and basically just ensure that things are being called in the expected order. |
| */ |
| @RunWith(RobolectricTestRunner.class) |
| public class SynchronousFileStorageTest extends FileStorageTestBase { |
| |
| private SynchronousFileStorage storage; |
| private final Context context = ApplicationProvider.getApplicationContext(); |
| |
| @Override |
| protected void initStorage() { |
| storage = |
| new SynchronousFileStorage( |
| ImmutableList.of(fileBackend, cnsBackend), |
| ImmutableList.of(compressTransform, encryptTransform, identityTransform), |
| ImmutableList.of(countingMonitor)); |
| } |
| |
| // Backend registrar |
| |
| @Test |
| public void registeredBackends_shouldNotThrowException() throws Exception { |
| assertThat(storage.exists(file1Uri)).isFalse(); |
| } |
| |
| @Test |
| public void unregisteredBackends_shouldThrowException() throws Exception { |
| Uri unregisteredUri = Uri.parse("unregistered:///"); |
| assertThrows(UnsupportedFileStorageOperation.class, () -> storage.exists(unregisteredUri)); |
| } |
| |
| @Test |
| public void nullUriScheme_shouldThrowException() throws Exception { |
| Uri relativeUri = Uri.parse("/relative/uri"); |
| assertThrows(UnsupportedFileStorageOperation.class, () -> storage.exists(relativeUri)); |
| } |
| |
| @Test |
| public void emptyBackendName_shouldBeSilentlySkipped() throws Exception { |
| Backend emptyNameBackend = |
| new ForwardingBackend() { |
| @Override |
| protected Backend delegate() { |
| return fileBackend; |
| } |
| |
| @Override |
| public String name() { |
| return ""; |
| } |
| }; |
| new SynchronousFileStorage(ImmutableList.of(emptyNameBackend)); |
| } |
| |
| @Test |
| public void doubleRegisteringBackendName_shouldThrowException() throws Exception { |
| assertThrows( |
| IllegalArgumentException.class, |
| () -> |
| new SynchronousFileStorage( |
| ImmutableList.of(new JavaFileBackend(), new JavaFileBackend()))); |
| } |
| |
| // Backend operations |
| |
| @Test |
| public void deleteFile_shouldInvokeBackend() throws Exception { |
| storage.deleteFile(file1Uri); |
| verify(fileBackend).deleteFile(file1Uri); |
| } |
| |
| @Test |
| public void deleteDir_shouldInvokeBackend() throws Exception { |
| storage.deleteDirectory(file1Uri); |
| verify(fileBackend).deleteDirectory(file1Uri); |
| } |
| |
| @Test |
| public void deleteRecursively_shouldRecurse() throws Exception { |
| Uri dir2Uri = dir1Uri.buildUpon().appendPath("dir2").build(); |
| when(fileBackend.exists(dir1Uri)).thenReturn(true); |
| when(fileBackend.isDirectory(dir1Uri)).thenReturn(true); |
| when(fileBackend.exists(dir2Uri)).thenReturn(true); |
| when(fileBackend.isDirectory(dir2Uri)).thenReturn(true); |
| when(fileBackend.exists(file1Uri)).thenReturn(true); |
| when(fileBackend.exists(file2Uri)).thenReturn(true); |
| when(fileBackend.children(dir1Uri)).thenReturn(ImmutableList.of(file1Uri, file2Uri, dir2Uri)); |
| when(fileBackend.children(dir2Uri)).thenReturn(Collections.emptyList()); |
| |
| assertThat(storage.deleteRecursively(dir1Uri)).isTrue(); |
| |
| verify(fileBackend).deleteFile(file1Uri); |
| verify(fileBackend).deleteFile(file2Uri); |
| verify(fileBackend).deleteDirectory(dir2Uri); |
| verify(fileBackend).deleteDirectory(dir1Uri); |
| } |
| |
| @Test |
| public void deleteRecursively_failsOnAccessError() throws Exception { |
| when(fileBackend.exists(dir1Uri)).thenReturn(true); |
| when(fileBackend.isDirectory(dir1Uri)).thenReturn(true); |
| when(fileBackend.exists(file1Uri)).thenReturn(true); |
| when(fileBackend.exists(file2Uri)).thenReturn(true); |
| when(fileBackend.children(dir1Uri)).thenReturn(ImmutableList.of(file1Uri, file2Uri)); |
| doThrow(IOException.class).when(fileBackend).deleteFile(file2Uri); |
| |
| assertThrows(IOException.class, () -> storage.deleteRecursively(dir1Uri)); |
| |
| verify(fileBackend).deleteFile(file1Uri); |
| verify(fileBackend).deleteFile(file2Uri); |
| verify(fileBackend, never()).deleteDirectory(dir1Uri); |
| } |
| |
| @Test |
| public void deleteRecursively_fileDeletes() throws Exception { |
| when(fileBackend.exists(file1Uri)).thenReturn(true); |
| when(fileBackend.isDirectory(file1Uri)).thenReturn(false); |
| |
| assertThat(storage.deleteRecursively(file1Uri)).isTrue(); |
| |
| verify(fileBackend).exists(file1Uri); |
| verify(fileBackend).deleteFile(file1Uri); |
| } |
| |
| @Test |
| public void deleteRecursively_fileNotExist() throws Exception { |
| when(fileBackend.exists(dir1Uri)).thenReturn(false); |
| |
| assertThat(storage.deleteRecursively(dir1Uri)).isFalse(); |
| |
| verify(fileBackend).exists(dir1Uri); |
| } |
| |
| @Test |
| public void rename_shouldInvokeBackend() throws Exception { |
| storage.rename(file1Uri, file2Uri); |
| verify(fileBackend).rename(file1Uri, file2Uri); |
| } |
| |
| @Test |
| public void rename_crossingBackendsShouldThrowException() throws Exception { |
| assertThrows(UnsupportedFileStorageOperation.class, () -> storage.rename(file1Uri, cnsUri)); |
| } |
| |
| @Test |
| public void exists_shouldInvokeBackend() throws Exception { |
| assertThat(storage.exists(file1Uri)).isFalse(); |
| verify(fileBackend).exists(file1Uri); |
| } |
| |
| @Test |
| public void isDirectory_shouldInvokeBackend() throws Exception { |
| assertThat(storage.isDirectory(file1Uri)).isFalse(); |
| verify(fileBackend).isDirectory(file1Uri); |
| } |
| |
| @Test |
| public void createDirectoryshouldInvokeBackend() throws Exception { |
| storage.createDirectory(file1Uri); |
| verify(fileBackend).createDirectory(file1Uri); |
| } |
| |
| @Test |
| public void fileSize_shouldInvokeBackend() throws Exception { |
| assertThat(storage.fileSize(file1Uri)).isEqualTo(0L); |
| verify(fileBackend).fileSize(file1Uri); |
| } |
| |
| // |
| // Transform stuff |
| // |
| |
| @Test |
| public void registeredTransforms_shouldNotThrowException() throws Exception { |
| assertThat(storage.exists(file1CompressUri)).isFalse(); |
| verify(fileBackend).exists(file1Uri); |
| } |
| |
| @Test |
| public void unregisteredTransforms_shouldThrowException() throws Exception { |
| Uri unregisteredUri = Uri.parse(file1Uri + "#transform=unregistered"); |
| assertThrows(UnsupportedFileStorageOperation.class, () -> storage.exists(unregisteredUri)); |
| } |
| |
| @Test |
| public void getDebugInfo_shouldIncludeRegisteredPlugins() throws Exception { |
| SynchronousFileStorage debugStorage = |
| new SynchronousFileStorage( |
| ImmutableList.of(new JavaFileBackend(), AndroidFileBackend.builder(context).build()), |
| ImmutableList.of(new CompressTransform(), new BufferTransform()), |
| ImmutableList.of(new BufferingMonitor(), new NoOpMonitor())); |
| String debugString = debugStorage.getDebugInfo(); |
| |
| assertThat(debugString) |
| .isEqualTo( |
| "Registered Mobstore Plugins:\n" |
| + "\n" |
| + "Backends:\n" |
| + "protocol: android, class: AndroidFileBackend,\n" |
| + "protocol: file, class: JavaFileBackend\n" |
| + "\n" |
| + "Transforms:\n" |
| + "BufferTransform,\n" |
| + "CompressTransform\n" |
| + "\n" |
| + "Monitors:\n" |
| + "BufferingMonitor,\n" |
| + "NoOpMonitor"); |
| } |
| |
| @Test |
| public void emptyTransformName_shouldBeSilentlySkipped() throws Exception { |
| Transform emptyNameTransform = |
| new Transform() { |
| @Override |
| public String name() { |
| return ""; |
| } |
| }; |
| new SynchronousFileStorage(ImmutableList.of(), ImmutableList.of(emptyNameTransform)); |
| } |
| |
| @Test |
| public void doubleRegisteringTransformName_shouldThrowException() throws Exception { |
| assertThrows( |
| IllegalArgumentException.class, |
| () -> |
| new SynchronousFileStorage( |
| ImmutableList.of(), |
| ImmutableList.of(new CompressTransform(), new CompressTransform()))); |
| } |
| |
| @Test |
| public void read_shouldInvokeTransforms() throws Exception { |
| when(compressTransform.wrapForRead(eqParam(uriWithCompressParam), any(InputStream.class))) |
| .thenReturn(compressInputStream); |
| try (InputStream in = storage.open(file1CompressUri, ReadStreamOpener.create())) { |
| verify(compressTransform).wrapForRead(eqParam(uriWithCompressParam), any(InputStream.class)); |
| verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename)); |
| } |
| } |
| |
| @Test |
| public void read_shouldInvokeTransformsWithEncoded() throws Exception { |
| when(compressTransform.wrapForRead( |
| eqParam(uriWithCompressParamWithEncoded), any(InputStream.class))) |
| .thenReturn(compressInputStream); |
| try (InputStream in = storage.open(file1CompressUriWithEncoded, ReadStreamOpener.create())) { |
| verify(compressTransform) |
| .wrapForRead(eqParam(uriWithCompressParamWithEncoded), any(InputStream.class)); |
| verify(compressTransform).encode(eqParam(uriWithCompressParamWithEncoded), eq(file1Filename)); |
| } |
| } |
| |
| @Test |
| public void write_shouldInvokeTransforms() throws Exception { |
| when(compressTransform.wrapForWrite(eqParam(uriWithCompressParam), any(OutputStream.class))) |
| .thenReturn(compressOutputStream); |
| try (OutputStream out = storage.open(file1CompressUri, WriteStreamOpener.create())) { |
| verify(compressTransform) |
| .wrapForWrite(eqParam(uriWithCompressParam), any(OutputStream.class)); |
| verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename)); |
| } |
| } |
| |
| @Test |
| public void deleteFile_shouldInvokeTransformEncode() throws Exception { |
| storage.deleteFile(file1CompressUri); |
| verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename)); |
| } |
| |
| @Test |
| public void deleteDirectory_shouldNOTInvokeTransformEncode() throws Exception { |
| storage.deleteDirectory(file1CompressUri); |
| verify(compressTransform, never()).encode(eqParam(uriWithCompressParam), any()); |
| } |
| |
| @Test |
| public void rename_shouldInvokeTransformEncode() throws Exception { |
| storage.rename(file1CompressUri, file2CompressEncryptUri); |
| verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename)); |
| verify(encryptTransform).encode(eqParam(uriWithEncryptParam), eq(file2Filename)); |
| verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file2Filename)); |
| } |
| |
| @Test |
| public void rename_shouldNOTInvokeTransformEncodeOnDirectory() throws Exception { |
| storage.rename(dir1Uri, dir2CompressUri); |
| verify(compressTransform, never()).encode(eqParam(uriWithCompressParam), any()); |
| } |
| |
| @Test |
| public void exists_shouldInvokeTransformEncode() throws Exception { |
| assertThat(storage.exists(file1CompressUri)).isFalse(); |
| verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename)); |
| } |
| |
| @Test |
| public void exists_shouldNOTInvokeTransformEncodeOnDirectory() throws Exception { |
| assertThat(storage.exists(dir2CompressUri)).isFalse(); |
| verify(compressTransform, never()).encode(eqParam(uriWithCompressParam), any()); |
| } |
| |
| @Test |
| public void isDirectory_shouldNOTInvokeTransformEncode() throws Exception { |
| assertThat(storage.isDirectory(file1CompressUri)).isFalse(); |
| verify(compressTransform, never()).encode(eqParam(uriWithCompressParam), any()); |
| } |
| |
| @Test |
| public void createDirectoryshouldNOTInvokeTransformEncode() throws Exception { |
| storage.createDirectory(file1CompressUri); |
| verify(compressTransform, never()).encode(eqParam(uriWithCompressParam), any()); |
| } |
| |
| @Test |
| public void fileSize_shouldInvokeTransformEncode() throws Exception { |
| assertThat(storage.fileSize(file1CompressUri)).isEqualTo(0L); |
| verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file1Filename)); |
| } |
| |
| @Test |
| public void multipleTransformsShouldBeEncodedForwardAndComposedInReverse() throws Exception { |
| // The spec "transform=compress+encrypt" means the data is compressed and then |
| // encrypted before stored. Since transforms are implemented by wrapping transforms, |
| // they need to be instantiated in the reverse order. So, in this case, |
| // 1. encrypt is instantiated |
| // 2. encrypt wraps the backend stream |
| // 3. compress is instantiated |
| // 4. compress wraps the encrypted stream |
| // 5. the compress transforms stream is returned to the client |
| // In contrast, encode() is called in the order in which transforms appear in the fragment. |
| |
| when(encryptTransform.wrapForWrite(eqParam(uriWithEncryptParam), any(OutputStream.class))) |
| .thenReturn(encryptOutputStream); |
| when(compressTransform.wrapForWrite(eqParam(uriWithCompressParam), eq(encryptOutputStream))) |
| .thenReturn(compressOutputStream); |
| try (OutputStream out = storage.open(file2CompressEncryptUri, WriteStreamOpener.create())) { |
| |
| InOrder forward = inOrder(compressTransform, encryptTransform); |
| forward.verify(compressTransform).encode(eqParam(uriWithCompressParam), eq(file2Filename)); |
| forward.verify(encryptTransform).encode(eqParam(uriWithEncryptParam), eq(file2Filename)); |
| |
| InOrder reverse = inOrder(encryptTransform, compressTransform); |
| reverse |
| .verify(encryptTransform) |
| .wrapForWrite(eqParam(uriWithEncryptParam), any(OutputStream.class)); |
| reverse |
| .verify(compressTransform) |
| .wrapForWrite(eqParam(uriWithCompressParam), eq(encryptOutputStream)); |
| } |
| } |
| |
| @Test |
| public void children_shouldInvokeTransformDecodeInReverse() throws Exception { |
| // The spec "transform=compress+encrypt" means the data is compressed and then encrypted. |
| // When listing children, transform decodes() are invoked in reverse. |
| |
| when(fileBackend.children(eq(file2Uri))).thenReturn(Arrays.asList(Uri.parse("file:///child1"))); |
| assertThat(storage.children(file2CompressEncryptUri)).isNotNull(); |
| |
| InOrder reverse = inOrder(encryptTransform, compressTransform); |
| reverse.verify(encryptTransform).decode(eqParam(uriWithEncryptParam), eq("child1")); |
| reverse.verify(compressTransform).decode(eqParam(uriWithCompressParam), eq("child1")); |
| } |
| |
| @Test |
| public void children_transformsShouldNotDecodeSubdirectories() throws Exception { |
| when(fileBackend.children(eq(file1Uri))) |
| .thenReturn( |
| Arrays.asList( |
| Uri.parse("file:///file1"), |
| Uri.parse("file:///file2"), |
| Uri.parse("file:///dir1/"))); |
| assertThat(storage.children(file1CompressUri)).isNotNull(); |
| |
| verify(compressTransform).decode(eqParam(uriWithCompressParam), eq("file1")); |
| verify(compressTransform).decode(eqParam(uriWithCompressParam), eq("file2")); |
| verify(compressTransform, never()).decode(eqParam(uriWithCompressParam), eq("dir1")); |
| verify(compressTransform, atLeast(1)).name(); |
| verifyNoMoreInteractions(compressTransform); |
| } |
| |
| // |
| // Monitor stuff |
| // |
| |
| @Test |
| public void read_shouldMonitor() throws Exception { |
| try (InputStream in = storage.open(file1Uri, ReadStreamOpener.create())) { |
| verify(countingMonitor).monitorRead(file1Uri); |
| } |
| } |
| |
| @Test |
| public void write_shouldMonitor() throws Exception { |
| try (OutputStream out = storage.open(file1Uri, WriteStreamOpener.create())) { |
| verify(countingMonitor).monitorWrite(file1Uri); |
| } |
| } |
| |
| @Test |
| public void append_shouldMonitor() throws Exception { |
| try (OutputStream out = storage.open(file1Uri, AppendStreamOpener.create())) { |
| verify(countingMonitor).monitorAppend(file1Uri); |
| } |
| } |
| |
| @Test |
| public void readWithTransform_shouldGetOriginalUri() throws Exception { |
| try (InputStream in = storage.open(file1CompressUri, ReadStreamOpener.create())) { |
| verify(countingMonitor).monitorRead(file1CompressUri); |
| } |
| } |
| |
| // |
| // MobStoreGc stuff |
| // |
| |
| @Test |
| public void gcMethods_shouldInvokeCorrespondingBackendMethods() throws Exception { |
| GcParam param = GcParam.expiresAt(new Date(1L)); |
| storage.setGcParam(file1Uri, param); |
| verify(fileBackend).setGcParam(eq(file1Uri), eq(param)); |
| storage.getGcParam(file1Uri); |
| verify(fileBackend).getGcParam(eq(file1Uri)); |
| } |
| } |