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