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