1 /*
2  * Copyright (C) 2010 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.common.test.mocks;
18 
19 import com.google.common.base.Preconditions;
20 import com.google.common.collect.Maps;
21 
22 import android.content.ContentValues;
23 import android.database.Cursor;
24 import android.database.MatrixCursor;
25 import android.net.Uri;
26 import android.support.annotation.Nullable;
27 
28 import junit.framework.Assert;
29 
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Iterator;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Objects;
36 
37 /**
38  * A programmable mock content provider.
39  */
40 public class MockContentProvider extends android.test.mock.MockContentProvider {
41     private static final String TAG = "MockContentProvider";
42 
43     public static class Query {
44 
45         private final Uri mUri;
46         private String[] mProjection;
47         private String[] mDefaultProjection;
48         private String mSelection;
49         private String[] mSelectionArgs;
50         private String mSortOrder;
51         private List<Object> mRows = new ArrayList<>();
52         private boolean mAnyProjection;
53         private boolean mAnySelection;
54         private boolean mAnySortOrder;
55         private boolean mAnyNumberOfTimes;
56 
57         private boolean mExecuted;
58 
Query(Uri uri)59         public Query(Uri uri) {
60             mUri = uri;
61         }
62 
63         @Override
toString()64         public String toString() {
65             return queryToString(mUri, mProjection, mSelection, mSelectionArgs, mSortOrder);
66         }
67 
withProjection(String... projection)68         public Query withProjection(String... projection) {
69             mProjection = projection;
70             return this;
71         }
72 
withDefaultProjection(String... projection)73         public Query withDefaultProjection(String... projection) {
74             mDefaultProjection = projection;
75             return this;
76         }
77 
withAnyProjection()78         public Query withAnyProjection() {
79             mAnyProjection = true;
80             return this;
81         }
82 
withSelection(String selection, String... selectionArgs)83         public Query withSelection(String selection, String... selectionArgs) {
84             mSelection = selection;
85             mSelectionArgs = selectionArgs;
86             return this;
87         }
88 
withAnySelection()89         public Query withAnySelection() {
90             mAnySelection = true;
91             return this;
92         }
93 
withSortOrder(String sortOrder)94         public Query withSortOrder(String sortOrder) {
95             mSortOrder = sortOrder;
96             return this;
97         }
98 
withAnySortOrder()99         public Query withAnySortOrder() {
100             mAnySortOrder = true;
101             return this;
102         }
103 
returnRow(ContentValues values)104         public Query returnRow(ContentValues values) {
105             mRows.add(values);
106             return this;
107         }
108 
returnRow(Object... row)109         public Query returnRow(Object... row) {
110             mRows.add(row);
111             return this;
112         }
113 
returnEmptyCursor()114         public Query returnEmptyCursor() {
115             mRows.clear();
116             return this;
117         }
118 
anyNumberOfTimes()119         public Query anyNumberOfTimes() {
120             mAnyNumberOfTimes = true;
121             return this;
122         }
123 
equals(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)124         public boolean equals(Uri uri, String[] projection, String selection,
125                 String[] selectionArgs, String sortOrder) {
126             if (!uri.equals(mUri)) {
127                 return false;
128             }
129 
130             if (!mAnyProjection && !Arrays.equals(projection, mProjection)) {
131                 return false;
132             }
133 
134             if (!mAnySelection && !Objects.equals(selection, mSelection)) {
135                 return false;
136             }
137 
138             if (!mAnySelection && !Arrays.equals(selectionArgs, mSelectionArgs)) {
139                 return false;
140             }
141 
142             if (!mAnySortOrder && !Objects.equals(sortOrder, mSortOrder)) {
143                 return false;
144             }
145 
146             return true;
147         }
148 
getResult(String[] projection)149         public Cursor getResult(String[] projection) {
150             String[] columnNames;
151             if (mAnyProjection) {
152                 columnNames = projection;
153             } else {
154                 columnNames = mProjection != null ? mProjection : mDefaultProjection;
155             }
156 
157             MatrixCursor cursor = new MatrixCursor(columnNames);
158             for (Object row : mRows) {
159                 if (row instanceof Object[]) {
160                     cursor.addRow((Object[]) row);
161                 } else {
162                     ContentValues values = (ContentValues) row;
163                     Object[] columns = new Object[projection.length];
164                     for (int i = 0; i < projection.length; i++) {
165                         columns[i] = values.get(projection[i]);
166                     }
167                     cursor.addRow(columns);
168                 }
169             }
170             return cursor;
171         }
172     }
173 
174     public static class TypeQuery {
175         private final Uri mUri;
176         private final String mType;
177 
TypeQuery(Uri uri, String type)178         public TypeQuery(Uri uri, String type) {
179             mUri = uri;
180             mType = type;
181         }
182 
getUri()183         public Uri getUri() {
184             return mUri;
185         }
186 
getType()187         public String getType() {
188             return mType;
189         }
190 
191         @Override
toString()192         public String toString() {
193             return mUri + " --> " + mType;
194         }
195 
equals(Uri uri)196         public boolean equals(Uri uri) {
197             return getUri().equals(uri);
198         }
199     }
200 
201     public static class Insert {
202         private final Uri mUri;
203         private final ContentValues mContentValues;
204         private final Uri mResultUri;
205         private boolean mAnyNumberOfTimes;
206         private boolean mIsExecuted;
207 
208         /**
209          * Creates a new Insert to expect.
210          *
211          * @param uri the uri of the insertion request.
212          * @param contentValues the ContentValues to insert.
213          * @param resultUri the {@link Uri} for the newly inserted item.
214          * @throws NullPointerException if any parameter is {@code null}.
215          */
Insert(Uri uri, ContentValues contentValues, Uri resultUri)216         public Insert(Uri uri, ContentValues contentValues, Uri resultUri) {
217             mUri = Preconditions.checkNotNull(uri);
218             mContentValues = Preconditions.checkNotNull(contentValues);
219             mResultUri = Preconditions.checkNotNull(resultUri);
220         }
221 
222         /**
223          * Causes this insert expectation to be useable for mutliple calls to insert, rather than
224          * just one.
225          *
226          * @return this
227          */
anyNumberOfTimes()228         public Insert anyNumberOfTimes() {
229             mAnyNumberOfTimes = true;
230             return this;
231         }
232 
equals(Uri uri, ContentValues contentValues)233         private boolean equals(Uri uri, ContentValues contentValues) {
234             return mUri.equals(uri) && mContentValues.equals(contentValues);
235         }
236 
237         @Override
equals(Object o)238         public boolean equals(Object o) {
239             if (this == o) {
240                 return true;
241             }
242             if (o == null || getClass() != o.getClass()) {
243                 return false;
244             }
245             Insert insert = (Insert) o;
246             return mAnyNumberOfTimes == insert.mAnyNumberOfTimes &&
247                     mIsExecuted == insert.mIsExecuted &&
248                     Objects.equals(mUri, insert.mUri) &&
249                     Objects.equals(mContentValues, insert.mContentValues) &&
250                     Objects.equals(mResultUri, insert.mResultUri);
251         }
252 
253         @Override
hashCode()254         public int hashCode() {
255             return Objects.hash(mUri, mContentValues, mResultUri, mAnyNumberOfTimes, mIsExecuted);
256         }
257 
258         @Override
toString()259         public String toString() {
260             return "Insert{" +
261                     "mUri=" + mUri +
262                     ", mContentValues=" + mContentValues +
263                     ", mResultUri=" + mResultUri +
264                     ", mAnyNumberOfTimes=" + mAnyNumberOfTimes +
265                     ", mIsExecuted=" + mIsExecuted +
266                     '}';
267         }
268     }
269 
270     public static class Delete {
271         private final Uri mUri;
272 
273         private boolean mAnyNumberOfTimes;
274         private boolean mAnySelection;
275         @Nullable private String mSelection;
276         @Nullable private String[] mSelectionArgs;
277         private boolean mIsExecuted;
278         private int mRowsAffected;
279 
280         /**
281          * Creates a new Delete to expect.
282          * @param uri the uri of the delete request.
283          * @throws NullPointerException if uri is {@code null}.
284          */
Delete(Uri uri)285         public Delete(Uri uri) {
286             mUri = Preconditions.checkNotNull(uri);
287         }
288 
289         /**
290          * Sets the given information as expected selection arguments.
291          *
292          * @param selection The selection to expect.
293          * @param selectionArgs The selection args to expect.
294          * @return this.
295          */
withSelection(String selection, @Nullable String[] selectionArgs)296         public Delete withSelection(String selection, @Nullable String[] selectionArgs) {
297             mSelection = Preconditions.checkNotNull(selection);
298             mSelectionArgs = selectionArgs;
299             mAnySelection = false;
300             return this;
301         }
302 
303         /**
304          * Sets this delete to expect any selection arguments.
305          *
306          * @return this.
307          */
withAnySelection()308         public Delete withAnySelection() {
309             mAnySelection = true;
310             return this;
311         }
312 
313         /**
314          * Sets this delete to return the given number of rows affected.
315          *
316          * @param rowsAffected The value to return when this expected delete is executed.
317          * @return this.
318          */
returnRowsAffected(int rowsAffected)319         public Delete returnRowsAffected(int rowsAffected) {
320             mRowsAffected = rowsAffected;
321             return this;
322         }
323 
324         /**
325          * Causes this delete expectation to be useable for multiple calls to delete, rather than
326          * just one.
327          *
328          * @return this.
329          */
anyNumberOfTimes()330         public Delete anyNumberOfTimes() {
331             mAnyNumberOfTimes = true;
332             return this;
333         }
334 
equals(Uri uri, String selection, String[] selectionArgs)335         private boolean equals(Uri uri, String selection, String[] selectionArgs) {
336             return mUri.equals(uri) && Objects.equals(mSelection, selection)
337                     && Arrays.equals(mSelectionArgs, selectionArgs);
338         }
339     }
340 
341     public static class Update {
342         private final Uri mUri;
343         private final ContentValues mContentValues;
344         @Nullable private String mSelection;
345         @Nullable private String[] mSelectionArgs;
346         private boolean mAnyNumberOfTimes;
347         private boolean mIsExecuted;
348         private int mRowsAffected;
349 
350         /**
351          * Creates a new Update to expect.
352          *
353          * @param uri the uri of the update request.
354          * @param contentValues the ContentValues to update.
355          *
356          * @throws NullPointerException if any parameter is {@code null}.
357          */
Update(Uri uri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)358         public Update(Uri uri,
359                       ContentValues contentValues,
360                       @Nullable String selection,
361                       @Nullable String[] selectionArgs) {
362             mUri = Preconditions.checkNotNull(uri);
363             mContentValues = Preconditions.checkNotNull(contentValues);
364             mSelection = selection;
365             mSelectionArgs = selectionArgs;
366         }
367 
368         /**
369          * Causes this update expectation to be useable for mutliple calls to update, rather than
370          * just one.
371          *
372          * @return this
373          */
anyNumberOfTimes()374         public Update anyNumberOfTimes() {
375             mAnyNumberOfTimes = true;
376             return this;
377         }
378 
379         /**
380          * Sets this update to return the given number of rows affected.
381          *
382          * @param rowsAffected The value to return when this expected update is executed.
383          * @return this.
384          */
returnRowsAffected(int rowsAffected)385         public Update returnRowsAffected(int rowsAffected) {
386             mRowsAffected = rowsAffected;
387             return this;
388         }
389 
equals(Uri uri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)390         private boolean equals(Uri uri,
391                                ContentValues contentValues,
392                                @Nullable String selection,
393                                @Nullable String[] selectionArgs) {
394             return mUri.equals(uri) && mContentValues.equals(contentValues) &&
395                     Objects.equals(mSelection, selection) &&
396                     Objects.equals(mSelectionArgs, selectionArgs);
397         }
398 
399         @Override
equals(Object o)400         public boolean equals(Object o) {
401             if (this == o) {
402                 return true;
403             }
404             if (o == null || getClass() != o.getClass()) {
405                 return false;
406             }
407             Update update = (Update) o;
408             return mAnyNumberOfTimes == update.mAnyNumberOfTimes &&
409                     mIsExecuted == update.mIsExecuted &&
410                     Objects.equals(mUri, update.mUri) &&
411                     Objects.equals(mContentValues, update.mContentValues) &&
412                     Objects.equals(mSelection, update.mSelection) &&
413                     Objects.equals(mSelectionArgs, update.mSelectionArgs);
414         }
415 
416         @Override
hashCode()417         public int hashCode() {
418             return Objects.hash(mUri, mContentValues, mAnyNumberOfTimes, mIsExecuted, mSelection,
419                     mSelectionArgs);
420         }
421 
422         @Override
toString()423         public String toString() {
424             return "Update{" +
425                     "mUri=" + mUri +
426                     ", mContentValues=" + mContentValues +
427                     ", mAnyNumberOfTimes=" + mAnyNumberOfTimes +
428                     ", mIsExecuted=" + mIsExecuted +
429                     ", mSelection=" + mSelection +
430                     ", mSelectionArgs=" + mSelectionArgs +
431                     '}';
432         }
433     }
434 
435     private List<Query> mExpectedQueries = new ArrayList<>();
436     private Map<Uri, String> mExpectedTypeQueries = Maps.newHashMap();
437     private List<Insert> mExpectedInserts = new ArrayList<>();
438     private List<Delete> mExpectedDeletes = new ArrayList<>();
439     private List<Update> mExpectedUpdates = new ArrayList<>();
440 
441     @Override
onCreate()442     public boolean onCreate() {
443         return true;
444     }
445 
expectQuery(Uri contentUri)446     public Query expectQuery(Uri contentUri) {
447         Query query = new Query(contentUri);
448         mExpectedQueries.add(query);
449         return query;
450     }
451 
expectTypeQuery(Uri uri, String type)452     public void expectTypeQuery(Uri uri, String type) {
453         mExpectedTypeQueries.put(uri, type);
454     }
455 
expectInsert(Uri contentUri, ContentValues contentValues, Uri resultUri)456     public void expectInsert(Uri contentUri, ContentValues contentValues, Uri resultUri) {
457         mExpectedInserts.add(new Insert(contentUri, contentValues, resultUri));
458     }
459 
expectUpdate(Uri contentUri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)460     public Update expectUpdate(Uri contentUri,
461                                ContentValues contentValues,
462                                @Nullable String selection,
463                                @Nullable String[] selectionArgs) {
464         Update update = new Update(contentUri, contentValues, selection, selectionArgs);
465         mExpectedUpdates.add(update);
466         return update;
467     }
468 
expectDelete(Uri contentUri)469     public Delete expectDelete(Uri contentUri) {
470         Delete delete = new Delete(contentUri);
471         mExpectedDeletes.add(delete);
472         return delete;
473     }
474 
475     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)476     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
477             String sortOrder) {
478         if (mExpectedQueries.isEmpty()) {
479             Assert.fail("Unexpected query: Actual:"
480                     + queryToString(uri, projection, selection, selectionArgs, sortOrder));
481         }
482 
483         for (Iterator<Query> iterator = mExpectedQueries.iterator(); iterator.hasNext();) {
484             Query query = iterator.next();
485             if (query.equals(uri, projection, selection, selectionArgs, sortOrder)) {
486                 query.mExecuted = true;
487                 if (!query.mAnyNumberOfTimes) {
488                     iterator.remove();
489                 }
490                 return query.getResult(projection);
491             }
492         }
493 
494         Assert.fail("Incorrect query. Expected one of: " + mExpectedQueries + ". Actual: " +
495                 queryToString(uri, projection, selection, selectionArgs, sortOrder));
496         return null;
497     }
498 
499     @Override
getType(Uri uri)500     public String getType(Uri uri) {
501         if (mExpectedTypeQueries.isEmpty()) {
502             Assert.fail("Unexpected getType query: " + uri);
503         }
504 
505         String mimeType = mExpectedTypeQueries.get(uri);
506         if (mimeType != null) {
507             return mimeType;
508         }
509 
510         Assert.fail("Unknown mime type for: " + uri);
511         return null;
512     }
513 
514     @Override
insert(Uri uri, ContentValues values)515     public Uri insert(Uri uri, ContentValues values) {
516         if (mExpectedInserts.isEmpty()) {
517             Assert.fail("Unexpected insert. Actual: " + insertToString(uri, values));
518         }
519         for (Iterator<Insert> iterator = mExpectedInserts.iterator(); iterator.hasNext(); ) {
520             Insert insert = iterator.next();
521             if (insert.equals(uri, values)) {
522                 insert.mIsExecuted = true;
523                 if (!insert.mAnyNumberOfTimes) {
524                     iterator.remove();
525                 }
526                 return insert.mResultUri;
527             }
528         }
529 
530         Assert.fail("Incorrect insert. Expected one of: " + mExpectedInserts + ". Actual: "
531                 + insertToString(uri, values));
532         return null;
533     }
534 
insertToString(Uri uri, ContentValues contentValues)535     private String insertToString(Uri uri, ContentValues contentValues) {
536         return "Insert { uri=" + uri + ", contentValues=" + contentValues + '}';
537     }
538 
539     @Override
update(Uri uri, ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)540     public int update(Uri uri,
541                       ContentValues values,
542                       @Nullable String selection,
543                       @Nullable String[] selectionArgs) {
544         if (mExpectedUpdates.isEmpty()) {
545             Assert.fail("Unexpected update. Actual: "
546                     + updateToString(uri, values, selection, selectionArgs));
547         }
548         for (Iterator<Update> iterator = mExpectedUpdates.iterator(); iterator.hasNext(); ) {
549             Update update = iterator.next();
550             if (update.equals(uri, values, selection, selectionArgs)) {
551                 update.mIsExecuted = true;
552                 if (!update.mAnyNumberOfTimes) {
553                     iterator.remove();
554                 }
555                 return update.mRowsAffected;
556             }
557         }
558 
559         Assert.fail("Incorrect update. Expected one of: " + mExpectedUpdates + ". Actual: "
560                 + updateToString(uri, values, selection, selectionArgs));
561         return - 1;
562     }
563 
updateToString(Uri uri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)564     private String updateToString(Uri uri,
565                                   ContentValues contentValues,
566                                   @Nullable String selection,
567                                   @Nullable String[] selectionArgs) {
568         return "Update { uri=" + uri + ", contentValues=" + contentValues + ", selection=" +
569                 selection + ", selectionArgs" + Arrays.toString(selectionArgs) + '}';
570     }
571 
572     @Override
delete(Uri uri, String selection, String[] selectionArgs)573     public int delete(Uri uri, String selection, String[] selectionArgs) {
574         if (mExpectedDeletes.isEmpty()) {
575             Assert.fail("Unexpected delete. Actual: " + deleteToString(uri, selection,
576                     selectionArgs));
577         }
578         for (Iterator<Delete> iterator = mExpectedDeletes.iterator(); iterator.hasNext(); ) {
579             Delete delete = iterator.next();
580             if (delete.equals(uri, selection, selectionArgs)) {
581                 delete.mIsExecuted = true;
582                 if (!delete.mAnyNumberOfTimes) {
583                     iterator.remove();
584                 }
585                 return delete.mRowsAffected;
586             }
587         }
588         Assert.fail("Incorrect delete. Expected one of: " + mExpectedDeletes + ". Actual: "
589                 + deleteToString(uri, selection, selectionArgs));
590         return -1;
591     }
592 
deleteToString(Uri uri, String selection, String[] selectionArgs)593     private String deleteToString(Uri uri, String selection, String[] selectionArgs) {
594         return "Delete { uri=" + uri + ", selection=" + selection + ", selectionArgs"
595                 + Arrays.toString(selectionArgs) + '}';
596     }
597 
queryToString(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)598     private static String queryToString(Uri uri, String[] projection, String selection,
599             String[] selectionArgs, String sortOrder) {
600         StringBuilder sb = new StringBuilder();
601         sb.append(uri).append(" ");
602         if (projection != null) {
603             sb.append(Arrays.toString(projection));
604         } else {
605             sb.append("[]");
606         }
607         if (selection != null) {
608             sb.append(" selection: '").append(selection).append("'");
609             if (selectionArgs != null) {
610                 sb.append(Arrays.toString(selectionArgs));
611             } else {
612                 sb.append("[]");
613             }
614         }
615         if (sortOrder != null) {
616             sb.append(" sort: '").append(sortOrder).append("'");
617         }
618         return sb.toString();
619     }
620 
verify()621     public void verify() {
622         verifyQueries();
623         verifyInserts();
624         verifyDeletes();
625     }
626 
verifyQueries()627     private void verifyQueries() {
628         List<Query> missedQueries = new ArrayList<>();
629         for (Query query : mExpectedQueries) {
630             if (!query.mExecuted) {
631                 missedQueries.add(query);
632             }
633         }
634         Assert.assertTrue("Not all expected queries have been called: " + missedQueries,
635                 missedQueries.isEmpty());
636     }
637 
verifyInserts()638     private void verifyInserts() {
639         List<Insert> missedInserts = new ArrayList<>();
640         for (Insert insert : mExpectedInserts) {
641             if (!insert.mIsExecuted) {
642                 missedInserts.add(insert);
643             }
644         }
645         Assert.assertTrue("Not all expected inserts have been called: " + missedInserts,
646                 missedInserts.isEmpty());
647     }
648 
verifyDeletes()649     private void verifyDeletes() {
650         List<Delete> missedDeletes = new ArrayList<>();
651         for (Delete delete : mExpectedDeletes) {
652             if (!delete.mIsExecuted) {
653                 missedDeletes.add(delete);
654             }
655         }
656         Assert.assertTrue("Not all expected deletes have been called: " + missedDeletes,
657                 missedDeletes.isEmpty());
658     }
659 }
660