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

Commit b97ea7ea authored by Bernardo Rufino's avatar Bernardo Rufino
Browse files

Add tests for PerformBackupTask

Tests for agent/transport interaction and some more.

Test: m -j RunFrameworksServicesRoboTests
Change-Id: Ie90044bdbdd32e6cfa70d6228841fec2d9fb0188
parent 44dbca09
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;
    }
}