Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 49cdc08b authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add tests for PerformBackupTask"

parents a9bf9d16 b97ea7ea
Loading
Loading
Loading
Loading
+176 −27
Original line number Diff line number Diff line
@@ -26,6 +26,8 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -40,7 +42,9 @@ import android.app.Application;
import android.app.IActivityManager;
import android.app.IBackupAgent;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupTransport;
import android.app.backup.FullBackupDataOutput;
import android.app.backup.IBackupManager;
import android.app.backup.IBackupManagerMonitor;
@@ -53,6 +57,7 @@ import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.PowerManager;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
@@ -80,6 +85,8 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
@@ -88,6 +95,7 @@ import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.shadows.ShadowQueuedWork;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
@@ -184,32 +192,33 @@ public class PerformBackupTaskTest {

    @Test
    public void testRunTask_whenTransportProvidesFlags_passesThemToTheAgent() throws Exception {
        BackupAgent agent = setUpAgent(PACKAGE_1);
        AgentMock agentMock = setUpAgent(PACKAGE_1);
        int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
        when(mTransportBinder.getTransportFlags()).thenReturn(flags);
        PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);

        runTask(task);

        verify(agent).onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any());
        verify(agentMock.agent)
                .onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any());
    }

    @Test
    public void testRunTask_whenTransportDoesNotProvidesFlags() throws Exception {
        BackupAgent agent = setUpAgent(PACKAGE_1);
        AgentMock agentMock = setUpAgent(PACKAGE_1);
        PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);

        runTask(task);

        verify(agent).onBackup(any(), argThat(dataOutputWithTransportFlags(0)), any());
        verify(agentMock.agent).onBackup(any(), argThat(dataOutputWithTransportFlags(0)), any());
    }

    @Test
    public void testRunTask_whenTransportProvidesFlagsAndMultipleAgents_passesToAll()
            throws Exception {
        List<BackupAgent> agents = setUpAgents(PACKAGE_1, PACKAGE_2);
        BackupAgent agent1 = agents.get(0);
        BackupAgent agent2 = agents.get(1);
        List<AgentMock> agentMocks = setUpAgents(PACKAGE_1, PACKAGE_2);
        BackupAgent agent1 = agentMocks.get(0).agent;
        BackupAgent agent2 = agentMocks.get(1).agent;
        int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
        when(mTransportBinder.getTransportFlags()).thenReturn(flags);
        PerformBackupTask task =
@@ -223,14 +232,103 @@ public class PerformBackupTaskTest {

    @Test
    public void testRunTask_whenTransportChangeFlagsAfterTaskCreation() throws Exception {
        BackupAgent agent = setUpAgent(PACKAGE_1);
        AgentMock agentMock = setUpAgent(PACKAGE_1);
        PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);
        int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
        when(mTransportBinder.getTransportFlags()).thenReturn(flags);

        runTask(task);

        verify(agent).onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any());
        verify(agentMock.agent)
                .onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any());
    }

    @Test
    public void testRunTask_callsListenerOnTaskFinished() throws Exception {
        setUpAgent(PACKAGE_1);
        PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);

        runTask(task);

        verify(mListener).onFinished(any());
    }

    @Test
    public void testRunTask_callsTransportPerformBackup() throws Exception {
        AgentMock agentMock = setUpAgent(PACKAGE_1);
        agentOnBackupDo(
                agentMock.agent,
                (oldState, dataOutput, newState) -> {
                    writeData(dataOutput, "key1", "foo".getBytes());
                    writeData(dataOutput, "key2", "bar".getBytes());
                });
        PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);
        // We need to verify at call time because the file is deleted right after
        when(mTransportBinder.performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
                .then(this::mockAndVerifyTransportPerformBackupData);

        runTask(task);

        // Already verified data in mockAndVerifyPerformBackupData
        verify(mTransportBinder).performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt());
    }

    private int mockAndVerifyTransportPerformBackupData(InvocationOnMock invocation)
            throws IOException {
        ParcelFileDescriptor data = invocation.getArgument(1);

        // Verifying that what we passed to the transport is what the agent wrote
        BackupDataInput dataInput = new BackupDataInput(data.getFileDescriptor());

        // "key1" => "foo"
        assertThat(dataInput.readNextHeader()).isTrue();
        assertThat(dataInput.getKey()).isEqualTo("key1");
        int size1 = dataInput.getDataSize();
        byte[] data1 = new byte[size1];
        dataInput.readEntityData(data1, 0, size1);
        assertThat(data1).isEqualTo("foo".getBytes());

        // "key2" => "bar"
        assertThat(dataInput.readNextHeader()).isTrue();
        assertThat(dataInput.getKey()).isEqualTo("key2");
        int size2 = dataInput.getDataSize();
        byte[] data2 = new byte[size2];
        dataInput.readEntityData(data2, 0, size2);
        assertThat(data2).isEqualTo("bar".getBytes());

        // No more
        assertThat(dataInput.readNextHeader()).isFalse();

        return BackupTransport.TRANSPORT_OK;
    }

    @Test
    public void testRunTask_whenPerformBackupSucceeds_callsTransportFinishBackup()
            throws Exception {
        setUpAgent(PACKAGE_1);
        PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);
        when(mTransportBinder.performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
                .thenReturn(BackupTransport.TRANSPORT_OK);

        runTask(task);

        verify(mTransportBinder).finishBackup();
    }

    @Test
    public void testRunTask_whenProhibitedKey_failsAgent() throws Exception {
        AgentMock agentMock = setUpAgent(PACKAGE_1);
        agentOnBackupDo(
                agentMock.agent,
                (oldState, dataOutput, newState) -> {
                    char prohibitedChar = 0xff00;
                    writeData(dataOutput, prohibitedChar + "key", "foo".getBytes());
                });
        PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);

        runTask(task);

        verify(agentMock.agentBinder).fail(any());
    }

    private void runTask(PerformBackupTask task) {
@@ -241,11 +339,12 @@ public class PerformBackupTaskTest {
        }
    }

    private List<BackupAgent> setUpAgents(String... packageNames) {
    private List<AgentMock> setUpAgents(String... packageNames) {
        return Stream.of(packageNames).map(this::setUpAgent).collect(toList());
    }

    private BackupAgent setUpAgent(String packageName) {
    private AgentMock setUpAgent(String packageName) {
        try {
            PackageInfo packageInfo = new PackageInfo();
            packageInfo.packageName = packageName;
            packageInfo.applicationInfo = new ApplicationInfo();
@@ -256,11 +355,18 @@ public class PerformBackupTaskTest {
                    packageName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
            mShadowPackageManager.addPackage(packageInfo);
            BackupAgent backupAgent = spy(BackupAgent.class);
        IBackupAgent backupAgentBinder = IBackupAgent.Stub.asInterface(backupAgent.onBind());
            IBackupAgent backupAgentBinder =
                    spy(IBackupAgent.Stub.asInterface(backupAgent.onBind()));
            // Don't crash our only process (in production code this would crash the app, not us)
            doNothing().when(backupAgentBinder).fail(any());
            when(mBackupManagerService.bindToAgentSynchronous(
                            eq(packageInfo.applicationInfo), anyInt()))
                    .thenReturn(backupAgentBinder);
        return backupAgent;
            return new AgentMock(backupAgentBinder, backupAgent);
        } catch (RemoteException e) {
            // Never happens, compiler happy
            throw new AssertionError(e);
        }
    }

    private PerformBackupTask createPerformBackupTask(
@@ -288,10 +394,53 @@ public class PerformBackupTaskTest {
        return task;
    }

    private ArgumentMatcher<BackupDataOutput> dataOutputWithTransportFlags(int flags) {
    private static ArgumentMatcher<PackageInfo> packageInfo(String packageName) {
        return packageInfo -> packageName.equals(packageInfo.packageName);
    }

    private static ArgumentMatcher<BackupDataOutput> dataOutputWithTransportFlags(int flags) {
        return dataOutput -> dataOutput.getTransportFlags() == flags;
    }

    private static void writeData(BackupDataOutput dataOutput, String key, byte[] data)
            throws IOException {
        dataOutput.writeEntityHeader(key, data.length);
        dataOutput.writeEntityData(data, data.length);
    }

    private static void agentOnBackupDo(BackupAgent agent, BackupAgentOnBackup function)
            throws Exception {
        doAnswer(function).when(agent).onBackup(any(), any(), any());
    }

    @FunctionalInterface
    private interface BackupAgentOnBackup extends Answer<Void> {
        void onBackup(
                ParcelFileDescriptor oldState,
                BackupDataOutput dataOutput,
                ParcelFileDescriptor newState)
                throws IOException;

        @Override
        default Void answer(InvocationOnMock invocation) throws Throwable {
            onBackup(
                    invocation.getArgument(0),
                    invocation.getArgument(1),
                    invocation.getArgument(2));
            return null;
        }
    }

    private static class AgentMock {
        private final IBackupAgent agentBinder;
        private final BackupAgent agent;

        public AgentMock(IBackupAgent agentBinder, BackupAgent agent) {
            this.agentBinder = agentBinder;
            this.agent = agent;
        }
    }

    private abstract static class FakeIBackupManager extends IBackupManager.Stub {
        private Handler mBackupHandler;
        private BackupRestoreTask mTask;
+44 −9
Original line number Diff line number Diff line
@@ -21,41 +21,76 @@ import android.app.backup.BackupDataInput;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

import java.io.EOFException;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

/**
 * Shadow for {@link BackupDataInput}. Format read does NOT match implementation. To write data to
 * be read by this shadow, you should also declare shadow {@link ShadowBackupDataOutput}.
 */
@Implements(BackupDataInput.class)
public class ShadowBackupDataInput {
    private ObjectInputStream mInput;
    private int mSize;
    private String mKey;
    private boolean mHeaderReady;

    @Implementation
    public void __constructor__(FileDescriptor fd) {
        try {
            mInput = new ObjectInputStream(new FileInputStream(fd));
        } catch (IOException e) {
            throw new AssertionError(e);
        }

    @Implementation
    protected void finalize() throws Throwable {
    }

    @Implementation
    public boolean readNextHeader() throws IOException {
        mHeaderReady = false;
        try {
            mSize = mInput.readInt();
        } catch (EOFException e) {
            return false;
        }
        mKey = mInput.readUTF();
        mHeaderReady = true;
        return true;
    }

    @Implementation
    public String getKey() {
        throw new AssertionError("Can't call because readNextHeader() returned false");
        checkHeaderReady();
        return mKey;
    }

    @Implementation
    public int getDataSize() {
        throw new AssertionError("Can't call because readNextHeader() returned false");
        checkHeaderReady();
        return mSize;
    }

    @Implementation
    public int readEntityData(byte[] data, int offset, int size) throws IOException {
        throw new AssertionError("Can't call because readNextHeader() returned false");
        checkHeaderReady();
        int result = mInput.read(data, offset, size);
        if (result < 0) {
            throw new IOException("result=0x" + Integer.toHexString(result));
        }
        return result;
    }

    @Implementation
    public void skipEntityData() throws IOException {
        throw new AssertionError("Can't call because readNextHeader() returned false");
        checkHeaderReady();
        mInput.read(new byte[mSize], 0, mSize);
    }

    private void checkHeaderReady() {
        if (!mHeaderReady) {
            throw new IllegalStateException("Entity header not read");
        }
    }
}
+31 −2
Original line number Diff line number Diff line
@@ -21,16 +21,29 @@ import android.app.backup.BackupDataOutput;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

/**
 * Shadow for {@link BackupDataOutput}. Format written does NOT match implementation. To read data
 * written with this shadow you should also declare shadow {@link ShadowBackupDataInput}.
 */
@Implements(BackupDataOutput.class)
public class ShadowBackupDataOutput {
    private long mQuota;
    private int mTransportFlags;
    private ObjectOutputStream mOutput;

    @Implementation
    public void __constructor__(FileDescriptor fd, long quota, int transportFlags) {
        try {
            mOutput = new ObjectOutputStream(new FileOutputStream(fd));
        } catch (IOException e) {
            throw new AssertionError(e);
        }
        mQuota = quota;
        mTransportFlags = transportFlags;
    }
@@ -47,11 +60,27 @@ public class ShadowBackupDataOutput {

    @Implementation
    public int writeEntityHeader(String key, int dataSize) throws IOException {
        return 0;
        final int size;
        try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) {
            writeEntityHeader(new ObjectOutputStream(byteStream), key, dataSize);
            size = byteStream.size();
        }
        writeEntityHeader(mOutput, key, dataSize);
        return size;
    }

    private void writeEntityHeader(ObjectOutputStream stream, String key, int dataSize)
            throws IOException {
        // Write the int first because readInt() throws EOFException, to know when stream ends
        stream.writeInt(dataSize);
        stream.writeUTF(key);
        stream.flush();
    }

    @Implementation
    public int writeEntityData(byte[] data, int size) throws IOException {
        return 0;
        mOutput.write(data, 0, size);
        mOutput.flush();
        return size;
    }
}