blob: 0743570da0c8681ac44dda13425fa75915bb52f6 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* 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 android.ext.services.common;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.sqlite.SQLiteDatabase;
import androidx.test.core.app.ApplicationProvider;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.google.common.truth.Expect;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.Spy;
import org.mockito.quality.Strictness;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.List;
public final class AdServicesFilesCleanupBootCompleteReceiverTest {
private static final String ADSERVICES_FILE_NAME = "adservices_file";
private static final String NON_ADSERVICES_FILE_NAME = "some_other_file";
private static final String NON_ADSERVICES_FILE_WITH_PREFIX_IN_NAME =
"some_file_with_adservices_in_name";
private static final String ADSERVICES_FILE_NAME_MIXED_CASE = "AdServicesFileMixedCase.txt";
private static final String NON_ADSERVICE_FILE_NAME_2 = "adservice_but_no_s.txt";
// Update this list with the previous name every time the receiver is renamed
private static final List<String> PREVIOUSLY_USED_CLASS_NAMES = List.of();
// TODO(b/297207132): Replace with AdServicesExtendedMockitoRule
private MockitoSession mMockitoSession;
@Spy
private final Context mContext = ApplicationProvider.getApplicationContext();
@Spy
private AdServicesFilesCleanupBootCompleteReceiver mReceiver;
@Mock
private PackageManager mPackageManager;
@Rule
public final Expect expect = Expect.create();
@Before
public void setup() {
mMockitoSession = ExtendedMockito.mockitoSession()
.initMocks(this)
.strictness(Strictness.WARN)
.startMocking();
doReturn(mPackageManager).when(mContext).getPackageManager();
}
@After
public void tearDown() {
if (mMockitoSession != null) {
mMockitoSession.finishMocking();
}
}
@Test
public void testReceiverDoesNotReuseClassNames() {
assertThat(PREVIOUSLY_USED_CLASS_NAMES)
.doesNotContain(AdServicesFilesCleanupBootCompleteReceiver.class.getName());
}
@Test
public void testReceiverSkipsDeletionIfDisabled() {
mockReceiverEnabled(false);
mReceiver.onReceive(mContext, /* intent= */ null);
verify(mContext, never()).getDataDir();
verify(mContext, never()).getPackageManager();
}
@Test
public void testReceiverDisablesItselfIfDeleteSuccessful() {
mockReceiverEnabled(true);
doNothing().when(mPackageManager).setComponentEnabledSetting(any(), anyInt(), anyInt());
doReturn(true).when(mReceiver).deleteAdServicesFiles(any());
mReceiver.onReceive(mContext, /* intent= */ null);
verifyDisableComponentCalled();
}
@Test
public void testReceiverDisablesItselfIfDeleteUnsuccessful() {
mockReceiverEnabled(true);
doReturn(false).when(mReceiver).deleteAdServicesFiles(any());
mReceiver.onReceive(mContext, /* intent= */ null);
verifyDisableComponentCalled();
}
@Test
public void testReceiverDeletesAdServicesFiles() throws Exception {
List<String> adServicesNames = List.of(ADSERVICES_FILE_NAME,
ADSERVICES_FILE_NAME_MIXED_CASE);
List<String> nonAdServicesNames = List.of(NON_ADSERVICES_FILE_NAME,
NON_ADSERVICES_FILE_WITH_PREFIX_IN_NAME, NON_ADSERVICE_FILE_NAME_2);
try {
createFiles(adServicesNames);
createFiles(nonAdServicesNames);
createDatabases(adServicesNames);
createDatabases(nonAdServicesNames);
mReceiver.deleteAdServicesFiles(mContext.getDataDir());
// Check if the appropriate files were deleted
String[] remainingFiles = mContext.getFilesDir().list();
List<String> remainingFilesList = Arrays.asList(remainingFiles);
expect.that(remainingFilesList).containsNoneIn(adServicesNames);
expect.that(remainingFilesList).containsAtLeastElementsIn(nonAdServicesNames);
expectDatabasesExist(nonAdServicesNames);
expectDatabasesDoNotExist(adServicesNames);
} finally {
deleteFiles(adServicesNames);
deleteFiles(nonAdServicesNames);
deleteDatabases(adServicesNames);
deleteDatabases(nonAdServicesNames);
}
}
@Test
public void testReceiverDeletesAdServicesDirectories() throws Exception {
String dataRoot = "data_root";
Path root = mContext.getFilesDir().toPath();
try {
File file1 = createFile(root, dataRoot, "level_1.txt"); // Preserved
File file2 = createFile(root, dataRoot, "adservices_level_1.txt"); // Deleted
File file3 = createFile(root, dataRoot + "/non_adservices",
"level_2.txt"); // Preserved
File file4 = createFile(root, dataRoot + "/non_adservices",
"adservices_level_2.txt"); // Deleted
File file5 = createFile(root, dataRoot + "/non_adservices/adservices_nested",
"level_3.txt"); // Deleted
File file6 = createFile(root, dataRoot + "/non_adservices/adservices_nested",
"adservices.level_3.txt"); // Deleted
File file7 = createFile(root, dataRoot + "/non_adservices",
"AdServices_level_2.txt"); // Deleted
File file8 = createFile(root, dataRoot + "/adservices-data",
"level_2.txt"); // Deleted
File file9 = createFile(root, dataRoot + "/adservices-data/nested",
"level_3.txt"); // Deleted
File file10 = createFile(root, dataRoot + "/AdServices-data/nested",
"level_3_1.txt");
mReceiver.deleteAdServicesFiles(mContext.getDataDir());
expectFilesExist(file1, file3);
expectFilesDoNotExist(file2, file4, file5, file6, file7, file8, file9, file10);
} finally {
deletePathRecursively(root.resolve(dataRoot));
}
}
@Test
public void testReceiverHandlesSecurityException() {
// Simulate a directory with three files, and the first one throws an exception on delete
File file1 = mock(File.class);
doReturn(ADSERVICES_FILE_NAME).when(file1).getName();
doThrow(SecurityException.class).when(file1).delete();
File file2 = mock(File.class);
doReturn(ADSERVICES_FILE_NAME_MIXED_CASE).when(file2).getName();
File file3 = mock(File.class);
doReturn(NON_ADSERVICES_FILE_NAME).when(file3).getName();
File dir = mock(File.class);
doReturn(true).when(dir).isDirectory();
doReturn(new File[] { file1, file2, file3 }).when(dir).listFiles();
// Execute the receiver
mReceiver.deleteAdServicesFiles(dir);
// Verify that deletion of both file1 and file2 was attempted, in spite of the exception
verify(file1).delete();
verify(file2).delete();
verify(file3, never()).delete();
}
@Test
public void testDeleteAdServicesFiles_invalidInput() {
// Null input
assertThat(mReceiver.deleteAdServicesFiles(null)).isTrue();
// Not a directory
File file = mock(File.class);
assertThat(mReceiver.deleteAdServicesFiles(file)).isTrue();
verify(file, never()).listFiles();
// Throws an exception
File file2 = mock(File.class);
doThrow(SecurityException.class).when(file2).isDirectory();
assertThat(mReceiver.deleteAdServicesFiles(file2)).isFalse();
verify(file2, never()).listFiles();
}
private void mockReceiverEnabled(boolean value) {
doReturn(value).when(mReceiver).isReceiverEnabled();
}
private void verifyDisableComponentCalled() {
verify(mPackageManager).setComponentEnabledSetting(any(),
eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), eq(0));
}
private void expectFilesExist(File... files) {
for (File file: files) {
expect.withMessage("%s exists", file.getPath()).that(file.exists()).isTrue();
}
}
private void expectFilesDoNotExist(File... files) {
for (File file: files) {
expect.withMessage("%s exists", file.getPath()).that(file.exists()).isFalse();
}
}
private void expectDatabasesExist(List<String> databaseNames) {
for (String db: databaseNames) {
expect.withMessage("%s exists", db)
.that(mContext.getDatabasePath(db).exists())
.isTrue();
}
}
private void expectDatabasesDoNotExist(List<String> databaseNames) {
for (String db: databaseNames) {
expect.withMessage("%s exists", db)
.that(mContext.getDatabasePath(db).exists())
.isFalse();
}
}
private void createFiles(List<String> names) throws Exception {
File dir = mContext.getFilesDir();
for (String name : names) {
createFile(name, dir);
}
}
private void createDatabases(List<String> names) {
for (String name : names) {
try (SQLiteDatabase unused = mContext.openOrCreateDatabase(name, 0, null)) {
// Intentionally do nothing.
}
}
}
private void deleteFiles(List<String> names) {
for (String name : names) {
File file = new File(mContext.getFilesDir(), name);
if (file.exists()) {
file.delete();
}
}
}
private void deleteDatabases(List<String> names) {
for (String name : names) {
mContext.deleteDatabase(name);
}
}
private File createFile(String name, File directory) throws Exception {
File file = new File(directory, name);
try (FileWriter writer = new FileWriter(file)) {
writer.append("test data");
writer.flush();
}
return file;
}
private File createFile(Path root, String path, String fileName) throws Exception {
Path dir = root.resolve(path);
Files.createDirectories(dir);
return createFile(fileName, dir.toFile());
}
private void deletePathRecursively(Path path) throws Exception {
Files.walkFileTree(path, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
}