1 /*
2  * Copyright (C) 2023 The Android Open Source Project
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 
17 package com.android.health.connect.backuprestore;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.mockito.ArgumentMatchers.any;
22 import static org.mockito.Mockito.never;
23 import static org.mockito.Mockito.spy;
24 import static org.mockito.Mockito.times;
25 import static org.mockito.Mockito.verify;
26 import static org.mockito.Mockito.when;
27 
28 import android.app.backup.BackupAgent;
29 import android.app.backup.BackupDataInput;
30 import android.app.backup.BackupDataOutput;
31 import android.app.backup.FullBackupDataOutput;
32 import android.content.Context;
33 import android.health.connect.HealthConnectManager;
34 import android.health.connect.restore.StageRemoteDataException;
35 import android.os.OutcomeReceiver;
36 import android.os.ParcelFileDescriptor;
37 import android.util.ArrayMap;
38 
39 import androidx.test.platform.app.InstrumentationRegistry;
40 import androidx.test.runner.AndroidJUnit4;
41 
42 import org.junit.Before;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 import org.mockito.ArgumentCaptor;
46 import org.mockito.Captor;
47 import org.mockito.Mock;
48 import org.mockito.MockitoAnnotations;
49 
50 import java.io.File;
51 import java.io.FileWriter;
52 import java.io.IOException;
53 import java.util.ArrayList;
54 import java.util.List;
55 import java.util.Map;
56 import java.util.Set;
57 import java.util.stream.Collectors;
58 import java.util.stream.Stream;
59 
60 /** Unit test class for {@link HealthConnectBackupAgent} */
61 @RunWith(AndroidJUnit4.class)
62 public class HealthConnectBackupAgentTest {
63     private TestableHealthConnectBackupAgent mHealthConnectBackupAgent;
64     @Mock private HealthConnectManager mHealthConnectManager;
65 
66     @Captor ArgumentCaptor<Map<String, ParcelFileDescriptor>> mPfdsByFileNameCaptor;
67     @Captor ArgumentCaptor<OutcomeReceiver<Void, StageRemoteDataException>> mStagingCallbackCaptor;
68     @Mock private Context mContext;
69     @Mock private FullBackupDataOutput mFullBackupDataOutput;
70     @Mock private ParcelFileDescriptor mOldState;
71     @Mock private ParcelFileDescriptor mNewState;
72     @Mock private BackupDataInput mBackupDataInput;
73     @Mock private BackupDataOutput mBackupDataOutput;
74     private File mBackupDataDirectory;
75 
76     @Before
setUp()77     public void setUp() {
78         MockitoAnnotations.initMocks(this);
79         mContext = InstrumentationRegistry.getInstrumentation().getContext();
80         mBackupDataDirectory = mContext.getDir("mock_data", Context.MODE_PRIVATE);
81 
82         mHealthConnectBackupAgent = new TestableHealthConnectBackupAgent();
83         mHealthConnectBackupAgent.onCreate();
84     }
85 
86     @Test
testOnRestoreFinished_stagingAll_sendsAllDataForStaging()87     public void testOnRestoreFinished_stagingAll_sendsAllDataForStaging() throws IOException {
88         createAndGetNonEmptyFile(mHealthConnectBackupAgent.getBackupDataDir(), "testFile1");
89         createAndGetNonEmptyFile(mHealthConnectBackupAgent.getBackupDataDir(), "testFile2");
90         Set<String> expectedStagedFileNames =
91                 Stream.of(mHealthConnectBackupAgent.getBackupDataDir().listFiles())
92                         .filter(file -> !file.isDirectory())
93                         .map(File::getName)
94                         .collect(Collectors.toSet());
95         assertThat(expectedStagedFileNames.size()).isEqualTo(2);
96 
97         Set<String> capturedStagedFileNames;
98 
99         mHealthConnectBackupAgent.onRestoreFinished();
100         verify(mHealthConnectManager, times(1))
101                 .stageAllHealthConnectRemoteData(mPfdsByFileNameCaptor.capture(), any(), any());
102         capturedStagedFileNames = mPfdsByFileNameCaptor.getValue().keySet();
103 
104         assertThat(capturedStagedFileNames.equals(expectedStagedFileNames)).isEqualTo(true);
105     }
106 
107     @Test
testOnRestoreFinished_afterStagingSuccess_deletesLocalData()108     public void testOnRestoreFinished_afterStagingSuccess_deletesLocalData() throws IOException {
109         HealthConnectBackupAgent spyForVerification = spy(mHealthConnectBackupAgent);
110         createAndGetNonEmptyFile(mHealthConnectBackupAgent.getBackupDataDir(), "testFile1");
111         Set<String> expectedStagedFileNames =
112                 Stream.of(mHealthConnectBackupAgent.getBackupDataDir().listFiles())
113                         .filter(file -> !file.isDirectory())
114                         .map(File::getName)
115                         .collect(Collectors.toSet());
116         assertThat(expectedStagedFileNames.size()).isEqualTo(1);
117 
118         mHealthConnectBackupAgent.onRestoreFinished();
119         verify(mHealthConnectManager, times(1))
120                 .stageAllHealthConnectRemoteData(any(), any(), mStagingCallbackCaptor.capture());
121         mStagingCallbackCaptor.getValue().onResult(null);
122 
123         verify(spyForVerification, times(1)).deleteBackupFiles();
124     }
125 
126     @Test
testOnRestoreFinished_afterStagingFailure_deletesLocalData()127     public void testOnRestoreFinished_afterStagingFailure_deletesLocalData() throws IOException {
128         HealthConnectBackupAgent spyForVerification = spy(mHealthConnectBackupAgent);
129         createAndGetNonEmptyFile(mHealthConnectBackupAgent.getBackupDataDir(), "testFile1");
130         Set<String> expectedStagedFileNames =
131                 Stream.of(mHealthConnectBackupAgent.getBackupDataDir().listFiles())
132                         .filter(file -> !file.isDirectory())
133                         .map(File::getName)
134                         .collect(Collectors.toSet());
135         assertThat(expectedStagedFileNames.size()).isEqualTo(1);
136 
137         mHealthConnectBackupAgent.onRestoreFinished();
138         verify(mHealthConnectManager, times(1))
139                 .stageAllHealthConnectRemoteData(any(), any(), mStagingCallbackCaptor.capture());
140         mStagingCallbackCaptor.getValue().onError(new StageRemoteDataException(new ArrayMap<>()));
141 
142         verify(spyForVerification, times(1)).deleteBackupFiles();
143     }
144 
145     @Test
testOnFullBackup_backsUpEverything()146     public void testOnFullBackup_backsUpEverything() throws Exception {
147         createAndGetNonEmptyFile(mBackupDataDirectory, "testFile1");
148         createAndGetNonEmptyFile(mBackupDataDirectory, "testFile2");
149 
150         mHealthConnectBackupAgent.onFullBackup(mFullBackupDataOutput);
151 
152         assertThat(mHealthConnectBackupAgent.mBackedUpFiles).hasSize(2);
153     }
154 
155     @Test
testOnFullBackup_deviceToDeviceFlag_callsBackupFileNamesWithTrue()156     public void testOnFullBackup_deviceToDeviceFlag_callsBackupFileNamesWithTrue()
157             throws Exception {
158         createAndGetNonEmptyFile(mBackupDataDirectory, "testFile1");
159         createAndGetNonEmptyFile(mBackupDataDirectory, "testFile2");
160 
161         when(mFullBackupDataOutput.getTransportFlags())
162                 .thenReturn(BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER);
163         mHealthConnectBackupAgent.onFullBackup(mFullBackupDataOutput);
164 
165         verify(mHealthConnectManager, times(1)).getAllBackupFileNames(true);
166     }
167 
168     @Test
testOnFullBackup_noDeviceToDeviceFlag_callsBackupFileNamesWithFalse()169     public void testOnFullBackup_noDeviceToDeviceFlag_callsBackupFileNamesWithFalse()
170             throws Exception {
171         createAndGetNonEmptyFile(mBackupDataDirectory, "testFile1");
172         createAndGetNonEmptyFile(mBackupDataDirectory, "testFile2");
173 
174         when(mFullBackupDataOutput.getTransportFlags()).thenReturn(0);
175         mHealthConnectBackupAgent.onFullBackup(mFullBackupDataOutput);
176 
177         verify(mHealthConnectManager, times(1)).getAllBackupFileNames(false);
178     }
179 
180     @Test
testOnRestore_doesNotStageAnything()181     public void testOnRestore_doesNotStageAnything() throws IOException {
182         createAndGetNonEmptyFile(mHealthConnectBackupAgent.getBackupDataDir(), "testFile1");
183         createAndGetNonEmptyFile(mHealthConnectBackupAgent.getBackupDataDir(), "testFile2");
184         Set<String> expectedStagedFileNames =
185                 Stream.of(mHealthConnectBackupAgent.getBackupDataDir().listFiles())
186                         .filter(file -> !file.isDirectory())
187                         .map(File::getName)
188                         .collect(Collectors.toSet());
189         assertThat(expectedStagedFileNames.size()).isEqualTo(2);
190 
191         mHealthConnectBackupAgent.onRestore(mBackupDataInput, 1, mNewState);
192         verify(mHealthConnectManager, never())
193                 .stageAllHealthConnectRemoteData(mPfdsByFileNameCaptor.capture(), any(), any());
194     }
195 
createAndGetNonEmptyFile(File dir, String fileName)196     private static void createAndGetNonEmptyFile(File dir, String fileName) throws IOException {
197         File file = new File(dir, fileName);
198         FileWriter fileWriter = new FileWriter(file);
199         fileWriter.write("Contents of file " + fileName);
200         fileWriter.close();
201     }
202 
203     /** A testable {@link HealthConnectBackupAgent} */
204     public class TestableHealthConnectBackupAgent extends HealthConnectBackupAgent {
205         List<File> mBackedUpFiles = new ArrayList<>();
206 
207         @Override
getBaseContext()208         public Context getBaseContext() {
209             return mContext;
210         }
211 
212         @Override
getBackupDataDir()213         File getBackupDataDir() {
214             return mBackupDataDirectory;
215         }
216 
217         @Override
getHealthConnectService()218         HealthConnectManager getHealthConnectService() {
219             return mHealthConnectManager;
220         }
221 
222         @Override
backupFile(File file, FullBackupDataOutput data)223         void backupFile(File file, FullBackupDataOutput data) {
224             mBackedUpFiles.add(file);
225         }
226     }
227 }
228