1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 package org.mozilla.gecko.tests;
6 
7 import android.content.ContentProvider;
8 import android.content.ContentUris;
9 import android.content.ContentValues;
10 import android.database.Cursor;
11 import android.database.sqlite.SQLiteDatabase;
12 import android.net.Uri;
13 
14 import org.mozilla.gecko.db.BrowserContract;
15 import org.mozilla.gecko.db.BrowserContract.DeletedLogins;
16 import org.mozilla.gecko.db.BrowserContract.Logins;
17 import org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts;
18 import org.mozilla.gecko.db.LoginsProvider;
19 
20 import java.util.concurrent.Callable;
21 
22 import static org.mozilla.gecko.db.BrowserContract.CommonColumns._ID;
23 import static org.mozilla.gecko.db.BrowserContract.DeletedLogins.TABLE_DELETED_LOGINS;
24 import static org.mozilla.gecko.db.BrowserContract.Logins.TABLE_LOGINS;
25 import static org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts.TABLE_DISABLED_HOSTS;
26 
27 public class testLoginsProvider extends ContentProviderTest {
28 
29     private static final String DB_NAME = "browser.db";
30 
31     private final TestCase[] TESTS_TO_RUN = {
32             new InsertLoginsTest(),
33             new UpdateLoginsTest(),
34             new DeleteLoginsTest(),
35             new InsertDeletedLoginsTest(),
36             new InsertDeletedLoginsFailureTest(),
37             new DisabledHostsInsertTest(),
38             new DisabledHostsInsertFailureTest(),
39             new InsertLoginsWithDefaultValuesTest(),
40             new InsertLoginsWithDuplicateGuidFailureTest(),
41             new DeleteLoginsByNonExistentGuidTest(),
42     };
43 
44     /**
45      * Factory function that makes new LoginsProvider instances.
46      * <p>
47      * We want a fresh provider each test, so this should be invoked in
48      * <code>setUp</code> before each individual test.
49      */
50     private static final Callable<ContentProvider> sProviderFactory = new Callable<ContentProvider>() {
51         @Override
52         public ContentProvider call() {
53             return new LoginsProvider();
54         }
55     };
56 
57     @Override
setUp()58     public void setUp() throws Exception {
59         super.setUp(sProviderFactory, BrowserContract.LOGINS_AUTHORITY, DB_NAME);
60         for (TestCase test: TESTS_TO_RUN) {
61             mTests.add(test);
62         }
63     }
64 
testLoginProviderTests()65     public void testLoginProviderTests() throws Exception {
66         for (Runnable test : mTests) {
67             final String testName = test.getClass().getSimpleName();
68             setTestName(testName);
69             ensureEmptyDatabase();
70             mAsserter.dumpLog("testLoginsProvider: Database empty - Starting " + testName + ".");
71             test.run();
72         }
73     }
74 
75     /**
76      * Wipe DB.
77      */
ensureEmptyDatabase()78     private void ensureEmptyDatabase() {
79         getWritableDatabase(Logins.CONTENT_URI).delete(TABLE_LOGINS, null, null);
80         getWritableDatabase(DeletedLogins.CONTENT_URI).delete(TABLE_DELETED_LOGINS, null, null);
81         getWritableDatabase(LoginsDisabledHosts.CONTENT_URI).delete(TABLE_DISABLED_HOSTS, null, null);
82     }
83 
getWritableDatabase(Uri uri)84     private SQLiteDatabase getWritableDatabase(Uri uri) {
85         Uri testUri = appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1");
86         DelegatingTestContentProvider delegateProvider = (DelegatingTestContentProvider) mProvider;
87         LoginsProvider loginsProvider = (LoginsProvider) delegateProvider.getTargetProvider();
88         return loginsProvider.getWritableDatabaseForTesting(testUri);
89     }
90 
91     /**
92      * LoginsProvider insert logins test.
93      */
94     private class InsertLoginsTest extends TestCase {
95         @Override
test()96         public void test() throws Exception {
97             ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
98                     "http://www.example.com", "username1", "password1", "username1", "password1", "guid1");
99             long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
100             verifyLoginExists(contentValues, id);
101             Cursor cursor = mProvider.query(Logins.CONTENT_URI, null, Logins.GUID + " = ?", new String[] { "guid1" }, null);
102             verifyRowMatches(contentValues, cursor, "logins found");
103 
104             // Empty ("") encrypted username and password are valid.
105             contentValues = createLogin("http://www.example.com", "http://www.example.com",
106                     "http://www.example.com", "username1", "password1", "", "", "guid2");
107             id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
108             verifyLoginExists(contentValues, id);
109             cursor = mProvider.query(Logins.CONTENT_URI, null, Logins.GUID + " = ?", new String[] { "guid2" }, null);
110             verifyRowMatches(contentValues, cursor, "logins found");
111         }
112     }
113 
114     /**
115      * LoginsProvider updates logins test.
116      */
117     private class UpdateLoginsTest extends TestCase {
118         @Override
test()119         public void test() throws Exception {
120             final String guid1 = "guid1";
121             ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
122                     "http://www.example.com", "username1", "password1", "username1", "password1", guid1);
123             long timeBeforeCreated = System.currentTimeMillis();
124             long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
125             long timeAfterCreated = System.currentTimeMillis();
126             verifyLoginExists(contentValues, id);
127 
128             Cursor cursor = getLoginById(id);
129             try {
130                 mAsserter.ok(cursor.moveToFirst(), "cursor is not empty", "");
131                 verifyBounded(timeBeforeCreated, cursor.getLong(cursor.getColumnIndexOrThrow(Logins.TIME_CREATED)), timeAfterCreated);
132             } finally {
133                 cursor.close();
134             }
135 
136             contentValues.put(BrowserContract.Logins.ENCRYPTED_USERNAME, "username2");
137             contentValues.put(Logins.ENCRYPTED_PASSWORD, "password2");
138 
139             Uri updateUri = Logins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
140             int numUpdated = mProvider.update(updateUri, contentValues, null, null);
141             mAsserter.is(1, numUpdated, "Correct number updated");
142             verifyLoginExists(contentValues, id);
143 
144             contentValues.put(BrowserContract.Logins.ENCRYPTED_USERNAME, "username1");
145             contentValues.put(Logins.ENCRYPTED_PASSWORD, "password1");
146 
147             updateUri = Logins.CONTENT_URI;
148             numUpdated = mProvider.update(updateUri, contentValues, Logins.GUID + " = ?", new String[]{guid1});
149             mAsserter.is(1, numUpdated, "Correct number updated");
150             verifyLoginExists(contentValues, id);
151         }
152     }
153 
154     /**
155      * LoginsProvider deletion logins test.
156      * - inserts a new logins
157      * - deletes the logins and verify deleted-logins table has entry for deleted guid.
158      */
159     private class DeleteLoginsTest extends TestCase {
160         @Override
test()161         public void test() throws Exception {
162             final String guid1 = "guid1";
163             ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
164                     "http://www.example.com", "username1", "password1", "username1", "password1", guid1);
165             long id = ContentUris.parseId(mProvider.insert(Logins.CONTENT_URI, contentValues));
166             verifyLoginExists(contentValues, id);
167 
168             Uri deletedUri = Logins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
169             int numDeleted = mProvider.delete(deletedUri, null, null);
170             mAsserter.is(1, numDeleted, "Correct number deleted");
171             verifyNoRowExists(Logins.CONTENT_URI, "No login entry found");
172 
173             contentValues = new ContentValues();
174             contentValues.put(DeletedLogins.GUID, guid1);
175             Cursor cursor = mProvider.query(DeletedLogins.CONTENT_URI, null, null, null, null);
176             verifyRowMatches(contentValues, cursor, "deleted-login found");
177             cursor = mProvider.query(DeletedLogins.CONTENT_URI, null, DeletedLogins.GUID + " = ?", new String[] { guid1 }, null);
178             verifyRowMatches(contentValues, cursor, "deleted-login found");
179         }
180     }
181 
182     /**
183      * LoginsProvider re-insert logins test.
184      * - inserts a row into deleted-logins
185      * - insert the same login (matching guid) and verify deleted-logins table is empty.
186      */
187     private class InsertDeletedLoginsTest extends TestCase {
188         @Override
test()189         public void test() throws Exception {
190             ContentValues contentValues = new ContentValues();
191             contentValues.put(DeletedLogins.GUID, "guid1");
192             long id = ContentUris.parseId(mProvider.insert(DeletedLogins.CONTENT_URI, contentValues));
193             final Uri insertedUri = DeletedLogins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
194             Cursor cursor = mProvider.query(insertedUri, null, null, null, null);
195             verifyRowMatches(contentValues, cursor, "deleted-login found");
196             verifyNoRowExists(BrowserContract.Logins.CONTENT_URI, "No login entry found");
197 
198             contentValues = createLogin("http://www.example.com", "http://www.example.com",
199                     "http://www.example.com", "username1", "password1", "username1", "password1", "guid1");
200             id = ContentUris.parseId(mProvider.insert(Logins.CONTENT_URI, contentValues));
201             verifyLoginExists(contentValues, id);
202             verifyNoRowExists(DeletedLogins.CONTENT_URI, "No deleted-login entry found");
203         }
204     }
205 
206     /**
207      * LoginsProvider insert Deleted logins test.
208      * - inserts a row into deleted-login without GUID.
209      */
210     private class InsertDeletedLoginsFailureTest extends TestCase {
211         @Override
test()212         public void test() throws Exception {
213             ContentValues contentValues = new ContentValues();
214             try {
215                 mProvider.insert(DeletedLogins.CONTENT_URI, contentValues);
216                 fail("Failed to throw IllegalArgumentException while missing GUID");
217             } catch (Exception e) {
218                 mAsserter.is(e.getClass(), IllegalArgumentException.class, "IllegalArgumentException thrown for invalid GUID");
219             }
220         }
221     }
222 
223     /**
224      * LoginsProvider disabled host test.
225      * - inserts a disabled-host
226      * - delete the inserted disabled-host and verify disabled-hosts table is empty.
227      */
228     private class DisabledHostsInsertTest extends TestCase {
229         @Override
test()230         public void test() throws Exception {
231             final String hostname = "localhost";
232             final ContentValues contentValues = new ContentValues();
233             contentValues.put(LoginsDisabledHosts.HOSTNAME, hostname);
234             mProvider.insert(LoginsDisabledHosts.CONTENT_URI, contentValues);
235             final Uri insertedUri = LoginsDisabledHosts.CONTENT_URI.buildUpon().appendPath("hostname").appendPath(hostname).build();
236             final Cursor cursor = mProvider.query(insertedUri, null, null, null, null);
237             verifyRowMatches(contentValues, cursor, "disabled-hosts found");
238 
239             final Uri deletedUri = LoginsDisabledHosts.CONTENT_URI.buildUpon().appendPath("hostname").appendPath(hostname).build();
240             final int numDeleted = mProvider.delete(deletedUri, null, null);
241             mAsserter.is(1, numDeleted, "Correct number deleted");
242             verifyNoRowExists(LoginsDisabledHosts.CONTENT_URI, "No disabled-hosts entry found");
243         }
244     }
245 
246     /**
247      * LoginsProvider disabled host insert failure testcase.
248      * - inserts a disabled-host without providing hostname
249      */
250     private class DisabledHostsInsertFailureTest extends TestCase {
251         @Override
test()252         public void test() throws Exception {
253             final String hostname = "localhost";
254             final ContentValues contentValues = new ContentValues();
255             try {
256                 mProvider.insert(LoginsDisabledHosts.CONTENT_URI, contentValues);
257                 fail("Failed to throw IllegalArgumentException while missing hostname");
258             } catch (Exception e) {
259                 mAsserter.is(e.getClass(), IllegalArgumentException.class, "IllegalArgumentException thrown for invalid hostname");
260             }
261         }
262     }
263 
264     /**
265      * LoginsProvider login insertion with default values test.
266      * - insert a login missing GUID, FORM_SUBMIT_URL, HTTP_REALM and verify default values are set.
267      */
268     private class InsertLoginsWithDefaultValuesTest extends TestCase {
269         @Override
test()270         protected void test() throws Exception {
271             ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
272                     "http://www.example.com", "username1", "password1", "username1", "password1", null);
273             // Remove GUID, HTTP_REALM, FORM_SUBMIT_URL from content values
274             contentValues.remove(Logins.GUID);
275             contentValues.remove(Logins.FORM_SUBMIT_URL);
276             contentValues.remove(Logins.HTTP_REALM);
277 
278             long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
279             Cursor cursor = getLoginById(id);
280             assertNotNull(cursor);
281             cursor.moveToFirst();
282 
283             mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.GUID)), null, "GUID is not null");
284             mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.HTTP_REALM)), null, "HTTP_REALM is not null");
285             mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.FORM_SUBMIT_URL)), null, "FORM_SUBMIT_URL is not null");
286             mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_LAST_USED)), null, "TIME_LAST_USED is not null");
287             mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_CREATED)), null, "TIME_CREATED is not null");
288             mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_PASSWORD_CHANGED)), null, "TIME_PASSWORD_CHANGED is not null");
289             mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.ENC_TYPE)), "0", "ENC_TYPE is 0");
290             mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.TIMES_USED)), "0", "TIMES_USED is 0");
291 
292             // Verify other values.
293             verifyRowMatches(contentValues, cursor, "Updated login found");
294         }
295     }
296 
297     /**
298      * LoginsProvider login insertion with duplicate GUID test.
299      * - insert two different logins with same GUID and verify that only one login exists.
300      */
301     private class InsertLoginsWithDuplicateGuidFailureTest extends TestCase {
302         @Override
test()303         protected void test() throws Exception {
304             final String guid = "guid1";
305             ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
306                     "http://www.example.com", "username1", "password1", "username1", "password1", guid);
307             long id1 = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
308             verifyLoginExists(contentValues, id1);
309 
310             // Insert another login with duplicate GUID.
311             contentValues = createLogin("http://www.example2.com", "http://www.example2.com",
312                     "http://www.example2.com", "username2", "password2", "username2", "password2", guid);
313             Uri insertUri = mProvider.insert(Logins.CONTENT_URI, contentValues);
314             mAsserter.is(insertUri, null, "Duplicate Guid insertion id1");
315 
316             // Verify login with id1 still exists.
317             verifyLoginExists(contentValues, id1);
318         }
319     }
320 
321     /**
322      * LoginsProvider deletion by non-existent GUID test.
323      * - delete a login with random GUID and verify that no entry was deleted.
324      */
325     private class DeleteLoginsByNonExistentGuidTest extends TestCase {
326         @Override
test()327         protected void test() throws Exception {
328             Uri deletedUri = Logins.CONTENT_URI;
329             int numDeleted = mProvider.delete(deletedUri, Logins.GUID + "= ?", new String[] { "guid1" });
330             mAsserter.is(0, numDeleted, "Correct number deleted");
331         }
332     }
333 
verifyBounded(long left, long middle, long right)334     private void verifyBounded(long left, long middle, long right) {
335         mAsserter.ok(left <= middle, "Left <= middle", left + " <= " + middle);
336         mAsserter.ok(middle <= right, "Middle <= right", middle + " <= " + right);
337     }
338 
getById(Uri uri, long id, String[] projection)339     private Cursor getById(Uri uri, long id, String[] projection) {
340         return mProvider.query(uri, projection,
341                 _ID + " = ?",
342                 new String[] { String.valueOf(id) },
343                 null);
344     }
345 
getLoginById(long id)346     private Cursor getLoginById(long id) {
347         return getById(Logins.CONTENT_URI, id, null);
348     }
349 
verifyLoginExists(ContentValues contentValues, long id)350     private void verifyLoginExists(ContentValues contentValues, long id) {
351         Cursor cursor = getLoginById(id);
352         verifyRowMatches(contentValues, cursor, "Updated login found");
353     }
354 
verifyRowMatches(ContentValues contentValues, Cursor cursor, String name)355     private void verifyRowMatches(ContentValues contentValues, Cursor cursor, String name) {
356         try {
357             mAsserter.ok(cursor.moveToFirst(), name, "cursor is not empty");
358             CursorMatches(cursor, contentValues);
359         } finally {
360             cursor.close();
361         }
362     }
363 
verifyNoRowExists(Uri contentUri, String name)364     private void verifyNoRowExists(Uri contentUri, String name) {
365         Cursor cursor = mProvider.query(contentUri, null, null, null, null);
366         try {
367             mAsserter.is(0, cursor.getCount(), name);
368         } finally {
369             cursor.close();
370         }
371     }
372 
createLogin(String hostname, String httpRealm, String formSubmitUrl, String usernameField, String passwordField, String encryptedUsername, String encryptedPassword, String guid)373     private ContentValues createLogin(String hostname, String httpRealm, String formSubmitUrl,
374                                       String usernameField, String passwordField, String encryptedUsername,
375                                       String encryptedPassword, String guid) {
376         final ContentValues values = new ContentValues();
377         values.put(Logins.HOSTNAME, hostname);
378         values.put(Logins.HTTP_REALM, httpRealm);
379         values.put(Logins.FORM_SUBMIT_URL, formSubmitUrl);
380         values.put(Logins.USERNAME_FIELD, usernameField);
381         values.put(Logins.PASSWORD_FIELD, passwordField);
382         values.put(Logins.ENCRYPTED_USERNAME, encryptedUsername);
383         values.put(Logins.ENCRYPTED_PASSWORD, encryptedPassword);
384         values.put(Logins.GUID, guid);
385         return values;
386     }
387 }
388