1 /*
2  * Copyright (C) 2016 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.contacts;
18 
19 import android.content.ContentProviderOperation;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.OperationApplicationException;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.os.RemoteException;
29 import android.provider.ContactsContract;
30 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
31 import android.provider.ContactsContract.Data;
32 import android.test.InstrumentationTestCase;
33 
34 import androidx.test.filters.MediumTest;
35 
36 import com.android.contacts.model.account.AccountWithDataSet;
37 
38 import java.util.ArrayList;
39 import java.util.List;
40 
41 /**
42  * Tests of GroupsDaoImpl that perform DB operations directly against CP2
43  */
44 @MediumTest
45 public class GroupsDaoIntegrationTests extends InstrumentationTestCase {
46 
47     private ContentResolver mResolver;
48     private List<Uri> mTestRecords;
49 
50     @Override
setUp()51     protected void setUp() throws Exception {
52         super.setUp();
53 
54         mTestRecords = new ArrayList<>();
55         mResolver = getContext().getContentResolver();
56     }
57 
58     @Override
tearDown()59     protected void tearDown() throws Exception {
60         super.tearDown();
61 
62         // Cleanup anything leftover by the tests.
63         cleanupTestRecords();
64         mTestRecords.clear();
65     }
66 
test_createGroup_createsGroupWithCorrectTitle()67     public void test_createGroup_createsGroupWithCorrectTitle() throws Exception {
68         final ContactSaveService.GroupsDao sut = createDao();
69         final Uri uri = sut.create("Test Create Group", getLocalAccount());
70 
71         assertNotNull(uri);
72         assertGroupHasTitle(uri, "Test Create Group");
73     }
74 
test_deleteEmptyGroup_marksRowDeleted()75     public void test_deleteEmptyGroup_marksRowDeleted() throws Exception {
76         final ContactSaveService.GroupsDao sut = createDao();
77         final Uri uri = sut.create("Test Delete Group", getLocalAccount());
78 
79         assertEquals(1, sut.delete(uri));
80 
81         final Cursor cursor = mResolver.query(uri, null, null, null, null, null);
82         try {
83             cursor.moveToFirst();
84             assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow(
85                     ContactsContract.Groups.DELETED)));
86         } finally {
87             cursor.close();
88         }
89     }
90 
test_undoDeleteEmptyGroup_createsGroupWithMatchingTitle()91     public void test_undoDeleteEmptyGroup_createsGroupWithMatchingTitle() throws Exception {
92         final ContactSaveService.GroupsDao sut = createDao();
93         final Uri uri = sut.create("Test Undo Delete Empty Group", getLocalAccount());
94 
95         final Bundle undoData = sut.captureDeletionUndoData(uri);
96 
97         assertEquals(1, sut.delete(uri));
98 
99         final Uri groupUri = sut.undoDeletion(undoData);
100 
101         assertGroupHasTitle(groupUri, "Test Undo Delete Empty Group");
102     }
103 
test_deleteNonEmptyGroup_removesGroupAndMembers()104     public void test_deleteNonEmptyGroup_removesGroupAndMembers() throws Exception {
105         final ContactSaveService.GroupsDao sut = createDao();
106         final Uri groupUri = sut.create("Test delete non-empty group", getLocalAccount());
107 
108         final long groupId = ContentUris.parseId(groupUri);
109         addMemberToGroup(ContentUris.parseId(createRawContact()), groupId);
110         addMemberToGroup(ContentUris.parseId(createRawContact()), groupId);
111 
112         assertEquals(1, sut.delete(groupUri));
113 
114         final Cursor cursor = mResolver.query(Data.CONTENT_URI, null,
115                 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
116                 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) },
117                 null, null);
118 
119         try {
120             cursor.moveToFirst();
121             // This is more of a characterization test since our code isn't manually deleting
122             // the membership rows just the group but this still helps document the expected
123             // behavior.
124             assertEquals(0, cursor.getCount());
125         } finally {
126             cursor.close();
127         }
128     }
129 
test_undoDeleteNonEmptyGroup_restoresGroupAndMembers()130     public void test_undoDeleteNonEmptyGroup_restoresGroupAndMembers() throws Exception {
131         final ContactSaveService.GroupsDao sut = createDao();
132         final Uri groupUri = sut.create("Test undo delete non-empty group", getLocalAccount());
133 
134         final long groupId = ContentUris.parseId(groupUri);
135         addMemberToGroup(ContentUris.parseId(createRawContact()), groupId);
136         addMemberToGroup(ContentUris.parseId(createRawContact()), groupId);
137 
138         final Bundle undoData = sut.captureDeletionUndoData(groupUri);
139 
140         sut.delete(groupUri);
141 
142         final Uri recreatedGroup = sut.undoDeletion(undoData);
143 
144         final long newGroupId = ContentUris.parseId(recreatedGroup);
145 
146         final Cursor cursor = mResolver.query(Data.CONTENT_URI, null,
147                 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
148                 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(newGroupId) },
149                 null, null);
150 
151         try {
152             assertEquals(2, cursor.getCount());
153         } finally {
154             cursor.close();
155         }
156     }
157 
test_captureUndoDataForDeletedGroup_returnsEmptyBundle()158     public void test_captureUndoDataForDeletedGroup_returnsEmptyBundle() {
159         final ContactSaveService.GroupsDao sut = createDao();
160 
161         final Uri uri = sut.create("a deleted group", getLocalAccount());
162         sut.delete(uri);
163 
164         final Bundle undoData = sut.captureDeletionUndoData(uri);
165 
166         assertTrue(undoData.isEmpty());
167     }
168 
test_captureUndoDataForNonExistentGroup_returnsEmptyBundle()169     public void test_captureUndoDataForNonExistentGroup_returnsEmptyBundle() {
170         final ContactSaveService.GroupsDao sut = createDao();
171 
172         // This test could potentially be flaky if this ID exists for some reason. 10 is subtracted
173         // to reduce the likelihood of this happening; some other test may use Integer.MAX_VALUE
174         // or nearby values  to cover some special case or boundary condition.
175         final long nonExistentId = Integer.MAX_VALUE - 10;
176 
177         final Bundle undoData = sut.captureDeletionUndoData(ContentUris
178                 .withAppendedId(ContactsContract.Groups.CONTENT_URI, nonExistentId));
179 
180         assertTrue(undoData.isEmpty());
181     }
182 
test_undoWithEmptyBundle_doesNothing()183     public void test_undoWithEmptyBundle_doesNothing() {
184         final ContactSaveService.GroupsDao sut = createDao();
185 
186         final Uri uri = sut.undoDeletion(new Bundle());
187 
188         assertNull(uri);
189     }
190 
test_undoDeleteEmptyGroupWithMissingMembersKey_shouldRecreateGroup()191     public void test_undoDeleteEmptyGroupWithMissingMembersKey_shouldRecreateGroup() {
192         final ContactSaveService.GroupsDao sut = createDao();
193         final Uri groupUri = sut.create("Test undo delete null memberIds", getLocalAccount());
194 
195         final Bundle undoData = sut.captureDeletionUndoData(groupUri);
196         undoData.remove(ContactSaveService.GroupsDaoImpl.KEY_GROUP_MEMBERS);
197         sut.delete(groupUri);
198 
199         sut.undoDeletion(undoData);
200 
201         assertGroupWithTitleExists("Test undo delete null memberIds");
202     }
203 
assertGroupHasTitle(Uri groupUri, String title)204     private void assertGroupHasTitle(Uri groupUri, String title) {
205         final Cursor cursor = mResolver.query(groupUri,
206                 new String[] { ContactsContract.Groups.TITLE },
207                 ContactsContract.Groups.DELETED + "=?",
208                 new String[] { "0" }, null, null);
209         try {
210             assertTrue("Group does not have title \"" + title + "\"",
211                     cursor.getCount() == 1 && cursor.moveToFirst() &&
212                             title.equals(cursor.getString(0)));
213         } finally {
214             cursor.close();
215         }
216     }
217 
assertGroupWithTitleExists(String title)218     private void assertGroupWithTitleExists(String title) {
219         final Cursor cursor = mResolver.query(ContactsContract.Groups.CONTENT_URI, null,
220                 ContactsContract.Groups.TITLE + "=? AND " +
221                         ContactsContract.Groups.DELETED + "=?",
222                 new String[] { title, "0" }, null, null);
223         try {
224             assertTrue("No group exists with title \"" + title + "\"", cursor.getCount() > 0);
225         } finally {
226             cursor.close();
227         }
228     }
229 
createDao()230     public ContactSaveService.GroupsDao createDao() {
231         return new GroupsDaoWrapper(new ContactSaveService.GroupsDaoImpl(getContext()));
232     }
233 
createRawContact()234     private Uri createRawContact() {
235         final ContentValues values = new ContentValues();
236         values.putNull(ContactsContract.RawContacts.ACCOUNT_NAME);
237         values.putNull(ContactsContract.RawContacts.ACCOUNT_TYPE);
238         final Uri result = mResolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);
239         mTestRecords.add(result);
240         return result;
241     }
242 
addMemberToGroup(long rawContactId, long groupId)243     private Uri addMemberToGroup(long rawContactId, long groupId) {
244         final ContentValues values = new ContentValues();
245         values.put(Data.RAW_CONTACT_ID, rawContactId);
246         values.put(Data.MIMETYPE,
247                 GroupMembership.CONTENT_ITEM_TYPE);
248         values.put(GroupMembership.GROUP_ROW_ID, groupId);
249 
250         // Dont' need to add to testRecords because it will be cleaned up when parent raw_contact
251         // is deleted.
252         return mResolver.insert(Data.CONTENT_URI, values);
253     }
254 
getContext()255     private Context getContext() {
256         return getInstrumentation().getTargetContext();
257     }
258 
getLocalAccount()259     private AccountWithDataSet getLocalAccount() {
260         return new AccountWithDataSet(null, null, null);
261     }
262 
cleanupTestRecords()263     private void cleanupTestRecords() throws RemoteException, OperationApplicationException {
264         final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
265         for (Uri uri : mTestRecords) {
266             if (uri == null) continue;
267             ops.add(ContentProviderOperation
268                     .newDelete(uri.buildUpon()
269                             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
270                             .build())
271                     .build());
272         }
273         mResolver.applyBatch(ContactsContract.AUTHORITY, ops);
274     }
275 
276     private class GroupsDaoWrapper implements ContactSaveService.GroupsDao {
277         private final ContactSaveService.GroupsDao mDelegate;
278 
GroupsDaoWrapper(ContactSaveService.GroupsDao delegate)279         public GroupsDaoWrapper(ContactSaveService.GroupsDao delegate) {
280             mDelegate = delegate;
281         }
282 
283         @Override
create(String title, AccountWithDataSet account)284         public Uri create(String title, AccountWithDataSet account) {
285             final Uri result = mDelegate.create(title, account);
286             mTestRecords.add(result);
287             return result;
288         }
289 
290         @Override
delete(Uri groupUri)291         public int delete(Uri groupUri) {
292             return mDelegate.delete(groupUri);
293         }
294 
295         @Override
captureDeletionUndoData(Uri groupUri)296         public Bundle captureDeletionUndoData(Uri groupUri) {
297             return mDelegate.captureDeletionUndoData(groupUri);
298         }
299 
300         @Override
undoDeletion(Bundle undoData)301         public Uri undoDeletion(Bundle undoData) {
302             final Uri result = mDelegate.undoDeletion(undoData);
303             mTestRecords.add(result);
304             return result;
305         }
306     }
307 }
308