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