1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16package com.android.vcard.tests.testutils;
17
18import android.content.ContentResolver;
19import android.content.EntityIterator;
20import android.database.Cursor;
21import android.net.Uri;
22import android.test.AndroidTestCase;
23import android.test.mock.MockContext;
24import android.text.TextUtils;
25import android.util.Log;
26
27import com.android.vcard.VCardComposer;
28import com.android.vcard.VCardConfig;
29import com.android.vcard.VCardEntryConstructor;
30import com.android.vcard.VCardParser;
31import com.android.vcard.VCardUtils;
32import com.android.vcard.exception.VCardException;
33
34import junit.framework.TestCase;
35
36import java.io.ByteArrayInputStream;
37import java.io.IOException;
38import java.io.InputStream;
39import java.lang.reflect.Method;
40
41/**
42 * <p>
43 * The class lets users checks that given expected vCard data are same as given actual vCard data.
44 * Able to verify both vCard importer/exporter.
45 * </p>
46 * <p>
47 * First a user has to initialize the object by calling either
48 * {@link #initForImportTest(int, int)} or {@link #initForExportTest(int)}.
49 * "Round trip test" (import -> export -> import, or export -> import -> export) is not supported.
50 * </p>
51 */
52public class VCardVerifier {
53    private static final String LOG_TAG = "VCardVerifier";
54    private static final boolean DEBUG = true;
55
56    /**
57     * Special URI for testing.
58     */
59    /* package */ static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard";
60    private static final Uri VCARD_TEST_AUTHORITY_URI =
61            Uri.parse("content://" + VCARD_TEST_AUTHORITY);
62    /* package */ static final Uri CONTACTS_TEST_CONTENT_URI =
63            Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts");
64
65    private static class CustomMockContext extends MockContext {
66        final ContentResolver mResolver;
67        public CustomMockContext(ContentResolver resolver) {
68            mResolver = resolver;
69        }
70
71        @Override
72        public ContentResolver getContentResolver() {
73            return mResolver;
74        }
75    }
76
77    private final AndroidTestCase mAndroidTestCase;
78    private int mVCardType;
79    private boolean mIsDoCoMo;
80
81    // Only one of them must be non-empty.
82    private ExportTestResolver mExportTestResolver;
83    private InputStream mInputStream;
84
85    // To allow duplication, use list instead of set.
86    // When null, we don't need to do the verification.
87    private PropertyNodesVerifier mPropertyNodesVerifier;
88    private LineVerifier mLineVerifier;
89    private ContentValuesVerifier mContentValuesVerifier;
90    private boolean mInitialized;
91    private boolean mVerified = false;
92    private String mCharset;
93
94    private String mExceptionContents;
95
96    // Called by VCardTestsBase
97    public VCardVerifier(AndroidTestCase androidTestCase) {
98        mAndroidTestCase = androidTestCase;
99        mExportTestResolver = null;
100        mInputStream = null;
101        mInitialized = false;
102        mVerified = false;
103    }
104
105    // Should be called at the beginning of each import test.
106    public void initForImportTest(int vcardType, int resId) {
107        if (mInitialized) {
108            AndroidTestCase.fail("Already initialized");
109        }
110        mVCardType = vcardType;
111        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
112        setInputResourceId(resId);
113        mInitialized = true;
114    }
115
116    // Should be called at the beginning of each export test.
117    public void initForExportTest(int vcardType) {
118        initForExportTest(vcardType, "UTF-8");
119    }
120
121    public void initForExportTest(int vcardType, String charset) {
122        if (mInitialized) {
123            AndroidTestCase.fail("Already initialized");
124        }
125        mExportTestResolver = new ExportTestResolver(mAndroidTestCase);
126        mVCardType = vcardType;
127        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
128        mInitialized = true;
129        if (TextUtils.isEmpty(charset)) {
130            mCharset = "UTF-8";
131        } else {
132            mCharset = charset;
133        }
134    }
135
136    private void setInputResourceId(int resId) {
137        final InputStream inputStream =
138                mAndroidTestCase.getContext().getResources().openRawResource(resId);
139        if (inputStream == null) {
140            AndroidTestCase.fail("Wrong resId: " + resId);
141        }
142        setInputStream(inputStream);
143    }
144
145    private void setInputStream(InputStream inputStream) {
146        if (mExportTestResolver != null) {
147            AndroidTestCase.fail("addInputEntry() is called.");
148        } else if (mInputStream != null) {
149            AndroidTestCase.fail("InputStream is already set");
150        }
151        mInputStream = inputStream;
152    }
153
154    public ContactEntry addInputEntry() {
155        if (!mInitialized) {
156            AndroidTestCase.fail("Not initialized");
157        }
158        if (mInputStream != null) {
159            AndroidTestCase.fail("setInputStream is called");
160        }
161        return mExportTestResolver.addInputContactEntry();
162    }
163
164    public PropertyNodesVerifierElem addPropertyNodesVerifierElemWithoutVersion() {
165        if (!mInitialized) {
166            AndroidTestCase.fail("Not initialized");
167        }
168        if (mPropertyNodesVerifier == null) {
169            mPropertyNodesVerifier = new PropertyNodesVerifier(mAndroidTestCase);
170        }
171        return mPropertyNodesVerifier.addPropertyNodesVerifierElem();
172    }
173
174    public PropertyNodesVerifierElem addPropertyNodesVerifierElem() {
175        final PropertyNodesVerifierElem elem = addPropertyNodesVerifierElemWithoutVersion();
176        final String versionString;
177        if (VCardConfig.isVersion21(mVCardType)) {
178            versionString = "2.1";
179        } else if (VCardConfig.isVersion30(mVCardType)) {
180            versionString = "3.0";
181        } else if (VCardConfig.isVersion40(mVCardType)) {
182            versionString = "4.0";
183        } else {
184            throw new RuntimeException("Unexpected vcard type during a unit test");
185        }
186        elem.addExpectedNodeWithOrder("VERSION", versionString);
187
188        return elem;
189    }
190
191    public PropertyNodesVerifierElem addPropertyNodesVerifierElemWithEmptyName() {
192        if (!mInitialized) {
193            AndroidTestCase.fail("Not initialized");
194        }
195        final PropertyNodesVerifierElem elem = addPropertyNodesVerifierElem();
196        if (VCardConfig.isVersion40(mVCardType)) {
197            elem.addExpectedNodeWithOrder("FN", "");
198        } else if (VCardConfig.isVersion30(mVCardType)) {
199            elem.addExpectedNodeWithOrder("N", "");
200            elem.addExpectedNodeWithOrder("FN", "");
201        } else if (mIsDoCoMo) {
202            elem.addExpectedNodeWithOrder("N", "");
203        }
204        return elem;
205    }
206
207    public LineVerifierElem addLineVerifierElem() {
208        if (!mInitialized) {
209            AndroidTestCase.fail("Not initialized");
210        }
211        if (mLineVerifier == null) {
212            mLineVerifier = new LineVerifier(mAndroidTestCase, mVCardType);
213        }
214        return mLineVerifier.addLineVerifierElem();
215    }
216
217    public ContentValuesVerifierElem addContentValuesVerifierElem() {
218        if (!mInitialized) {
219            AndroidTestCase.fail("Not initialized");
220        }
221        if (mContentValuesVerifier == null) {
222            mContentValuesVerifier = new ContentValuesVerifier();
223        }
224
225        return mContentValuesVerifier.addElem(mAndroidTestCase);
226    }
227
228    public void addVCardExceptionVerifier(String contents) {
229        mExceptionContents = contents;
230    }
231
232    /**
233     * Sets up sub-verifiers correctly and tries to parse vCard as {@link InputStream}.
234     * Errors around InputStream must be handled outside this method.
235     *
236     * Used both from {@link #verifyForImportTest()} and from {@link #verifyForExportTest()}.
237     */
238    private void verifyWithInputStream(InputStream is) throws IOException {
239        try {
240            // Note: we must not specify charset toward vCard parsers. This code checks whether
241            // those parsers are able to encode given binary without any extra information for
242            // charset.
243            final VCardParser parser = VCardUtils.getAppropriateParser(mVCardType);
244            if (mContentValuesVerifier != null) {
245                final VCardEntryConstructor constructor = new VCardEntryConstructor(mVCardType);
246                constructor.addEntryHandler(mContentValuesVerifier);
247                parser.addInterpreter(constructor);
248            }
249            if (mPropertyNodesVerifier != null) {
250                parser.addInterpreter(mPropertyNodesVerifier);
251            }
252            parser.parse(is);
253            if (mExceptionContents != null) {
254                // exception contents exists, we expect an exception to occur.
255                AndroidTestCase.fail();
256            }
257        } catch (VCardException e) {
258            if (mExceptionContents != null) {
259                AndroidTestCase.assertTrue(e.getMessage().contains(mExceptionContents));
260            } else {
261                Log.e(LOG_TAG, "VCardException", e);
262                AndroidTestCase.fail("Unexpected VCardException: " + e.getMessage());
263            }
264        }
265    }
266
267    private void verifyOneVCardForExport(final String vcard) {
268        if (DEBUG) Log.d(LOG_TAG, vcard);
269        InputStream is = null;
270        try {
271            is = new ByteArrayInputStream(vcard.getBytes(mCharset));
272            verifyWithInputStream(is);
273        } catch (IOException e) {
274            AndroidTestCase.fail("Unexpected IOException: " + e.getMessage());
275        } finally {
276            if (is != null) {
277                try {
278                    is.close();
279                } catch (IOException e) {
280                    AndroidTestCase.fail("Unexpected IOException: " + e.getMessage());
281                }
282            }
283        }
284    }
285
286    public void verify() {
287        if (!mInitialized) {
288            TestCase.fail("Not initialized.");
289        }
290        if (mVerified) {
291            TestCase.fail("verify() was called twice.");
292        }
293
294        if (mInputStream != null) {
295            if (mExportTestResolver != null){
296                TestCase.fail("There are two input sources.");
297            }
298            verifyForImportTest();
299        } else if (mExportTestResolver != null){
300            verifyForExportTest();
301        } else {
302            TestCase.fail("No input is determined");
303        }
304        mVerified = true;
305    }
306
307    private void verifyForImportTest() {
308        if (mLineVerifier != null) {
309            AndroidTestCase.fail("Not supported now.");
310        }
311
312        try {
313            verifyWithInputStream(mInputStream);
314        } catch (IOException e) {
315            AndroidTestCase.fail("IOException was thrown: " + e.getMessage());
316        } finally {
317            if (mInputStream != null) {
318                try {
319                    mInputStream.close();
320                } catch (IOException e) {
321                }
322            }
323        }
324    }
325
326    public static EntityIterator mockGetEntityIteratorMethod(
327            final ContentResolver resolver,
328            final Uri uri, final String selection,
329            final String[] selectionArgs, final String sortOrder) {
330        if (ExportTestResolver.class.equals(resolver.getClass())) {
331            return ((ExportTestResolver)resolver).getProvider().queryEntities(
332                    uri, selection, selectionArgs, sortOrder);
333        }
334
335        Log.e(LOG_TAG, "Unexpected provider given.");
336        return null;
337    }
338
339    private Method getMockGetEntityIteratorMethod()
340            throws SecurityException, NoSuchMethodException {
341        return this.getClass().getMethod("mockGetEntityIteratorMethod",
342                ContentResolver.class, Uri.class, String.class, String[].class, String.class);
343    }
344
345    private void verifyForExportTest() {
346        final CustomMockContext context = new CustomMockContext(mExportTestResolver);
347        final ContentResolver resolver = context.getContentResolver();
348        final VCardComposer composer = new VCardComposer(context, mVCardType, mCharset);
349        // projection is ignored.
350        final Cursor cursor = resolver.query(CONTACTS_TEST_CONTENT_URI, null, null, null, null);
351        if (!composer.init(cursor)) {
352            AndroidTestCase.fail("init() failed. Reason: " + composer.getErrorReason());
353        }
354        AndroidTestCase.assertFalse(composer.isAfterLast());
355        try {
356            while (!composer.isAfterLast()) {
357                Method mockGetEntityIteratorMethod = null;
358                try {
359                    mockGetEntityIteratorMethod = getMockGetEntityIteratorMethod();
360                } catch (Exception e) {
361                    AndroidTestCase.fail("Exception thrown: " + e);
362                }
363                AndroidTestCase.assertNotNull(mockGetEntityIteratorMethod);
364                final String vcard = composer.createOneEntry(mockGetEntityIteratorMethod);
365                AndroidTestCase.assertNotNull(vcard);
366                if (mLineVerifier != null) {
367                    mLineVerifier.verify(vcard);
368                }
369                verifyOneVCardForExport(vcard);
370            }
371        } finally {
372            composer.terminate();
373        }
374    }
375}
376