diff --git a/build/mobile/robocop/Actions.java.in b/build/mobile/robocop/Actions.java.in index 0c123016921d..baeeabd91510 100644 --- a/build/mobile/robocop/Actions.java.in +++ b/build/mobile/robocop/Actions.java.in @@ -39,6 +39,9 @@ package @ANDROID_PACKAGE_NAME@; import java.util.List; +import java.util.ArrayList; + +import android.database.Cursor; public interface Actions { @@ -92,4 +95,9 @@ public interface Actions { void sendSpecialKey(SpecialKey key); void drag(int startingX, int endingX, int startingY, int endingY); + + /** + * Run a sql query on the specified database + */ + public Cursor querySql(String dbPath, String sql); } diff --git a/build/mobile/robocop/FennecMochitestAssert.java.in b/build/mobile/robocop/FennecMochitestAssert.java.in index 20199ec51109..58841a6a3ca2 100644 --- a/build/mobile/robocop/FennecMochitestAssert.java.in +++ b/build/mobile/robocop/FennecMochitestAssert.java.in @@ -166,21 +166,13 @@ public class FennecMochitestAssert implements Assert { } public void is(Object a, Object b, String name) { - boolean pass = a.equals(b); - String diag = "got " + a.toString() + ", expected " + b.toString(); - if (pass) { - diag = a.toString() + " should equal " + b.toString(); - } - ok(pass, name, diag); + boolean pass = checkObjectsEqual(a,b); + ok(pass, name, getEqualString(a,b, pass)); } public void isnot(Object a, Object b, String name) { - boolean pass = !a.equals(b); - String diag = "didn't expect " + a.toString() + ", but got it"; - if (pass) { - diag = a.toString() + " should not equal " + b.toString(); - } - ok(pass, name, diag); + boolean pass = checkObjectsNotEqual(a,b); + ok(pass, name, getNotEqualString(a,b,pass)); } public void ispixel(int actual, int r, int g, int b, String name) { @@ -207,21 +199,50 @@ public class FennecMochitestAssert implements Assert { } public void todo_is(Object a, Object b, String name) { - boolean pass = a.equals(b); - String diag = "got " + a.toString() + ", expected " + b.toString(); - if (pass) { - diag = a.toString() + " should equal " + b.toString(); - } - todo(pass, name, diag); + boolean pass = checkObjectsEqual(a,b); + todo(pass, name, getEqualString(a,b,pass)); } - + public void todo_isnot(Object a, Object b, String name) { - boolean pass = !a.equals(b); - String diag = "didn't expect " + a.toString() + ", but got it"; - if (pass) { - diag = a.toString() + " should not equal " + b.toString(); + boolean pass = checkObjectsNotEqual(a,b); + todo(pass, name, getNotEqualString(a,b,pass)); + } + + private boolean checkObjectsEqual(Object a, Object b) { + if (a == null || b == null) { + if (a == null && b == null) { + return true; + } + return false; + } else { + return a.equals(b); } - todo(pass, name, diag); + } + + private String getEqualString(Object a, Object b, boolean pass) { + if (pass) { + return a + " should equal " + b; + } + return "got " + a + ", expected " + b; + } + + private boolean checkObjectsNotEqual(Object a, Object b) { + if (a == null || b == null) { + if ((a == null && b != null) || (a != null && b == null)) { + return true; + } else { + return false; + } + } else { + return !a.equals(b); + } + } + + private String getNotEqualString(Object a, Object b, boolean pass) { + if(pass) { + return a + " should not equal " + b; + } + return "didn't expect " + a + ", but got it"; } public void info(String name, String message) { diff --git a/build/mobile/robocop/FennecNativeActions.java.in b/build/mobile/robocop/FennecNativeActions.java.in index 973a3fd70bb6..7f8d88de3174 100644 --- a/build/mobile/robocop/FennecNativeActions.java.in +++ b/build/mobile/robocop/FennecNativeActions.java.in @@ -40,19 +40,24 @@ package @ANDROID_PACKAGE_NAME@; import java.lang.Class; +import java.lang.ClassLoader; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.InvocationHandler; import java.lang.Long; +import java.util.concurrent.SynchronousQueue; +import java.util.ArrayList; import android.app.Activity; +import android.content.Context; import android.app.Instrumentation; +import android.database.Cursor; import android.os.SystemClock; import android.view.View; import android.view.KeyEvent; - -import java.util.concurrent.SynchronousQueue; +import android.util.Log; import org.json.*; @@ -74,6 +79,7 @@ public class FennecNativeActions implements Actions { private Method mSendGE; private Method mGetLayerClient; private Method mSetDrawListener; + private static final String LOGTAG = "FennecNativeActions"; public FennecNativeActions(Activity activity, Solo robocop, Instrumentation instrumentation) { mSolo = robocop; @@ -349,4 +355,36 @@ public class FennecNativeActions implements Actions { public void drag(int startingX, int endingX, int startingY, int endingY) { mSolo.drag(startingX, endingX, startingY, endingY, 10); } + + public Cursor querySql(String dbPath, String sql) { + try { + ClassLoader classLoader = mGeckoApp.getClassLoader(); + Class sqlClass = classLoader.loadClass("org.mozilla.gecko.sqlite.SQLiteBridge"); + Class stringClass = String.class; + Class stringArrayClass = String[].class; + Class appshell = classLoader.loadClass("org.mozilla.gecko.GeckoAppShell"); + Class contextClass = Context.class; + + Constructor bridgeConstructor = sqlClass.getConstructor(stringClass); + Method query = sqlClass.getMethod("rawQuery", stringClass, stringArrayClass); + Method loadSQLiteLibs = appshell.getMethod("loadSQLiteLibs", contextClass, stringClass); + + Object bridge = bridgeConstructor.newInstance(dbPath); + + String resourcePath = mGeckoApp.getApplication().getPackageResourcePath(); + loadSQLiteLibs.invoke(null, mGeckoApp, resourcePath); + return (Cursor)query.invoke(bridge, sql, null); + } catch(ClassNotFoundException ex) { + Log.e(LOGTAG, "Error getting class", ex); + } catch(NoSuchMethodException ex) { + Log.e(LOGTAG, "Error getting method", ex); + } catch(InvocationTargetException ex) { + Log.e(LOGTAG, "Error invoking method", ex); + } catch(InstantiationException ex) { + Log.e(LOGTAG, "Error calling constructor", ex); + } catch(IllegalAccessException ex) { + Log.e(LOGTAG, "Error using field", ex); + } + return null; + } } diff --git a/mobile/android/base/db/PasswordsProvider.java.in b/mobile/android/base/db/PasswordsProvider.java.in index 7ad3e2403053..6e452fa4cca8 100644 --- a/mobile/android/base/db/PasswordsProvider.java.in +++ b/mobile/android/base/db/PasswordsProvider.java.in @@ -168,9 +168,6 @@ public class PasswordsProvider extends GeckoProvider { values.put(Passwords.GUID, guid); } String nowString = new Long(now).toString(); - DBUtils.replaceKey(values, CommonColumns._ID, Passwords.ID, ""); - DBUtils.replaceKey(values, SyncColumns.DATE_CREATED, Passwords.TIME_CREATED, nowString); - DBUtils.replaceKey(values, SyncColumns.DATE_MODIFIED, Passwords.TIME_PASSWORD_CHANGED, nowString); DBUtils.replaceKey(values, null, Passwords.HOSTNAME, ""); DBUtils.replaceKey(values, null, Passwords.HTTP_REALM, ""); DBUtils.replaceKey(values, null, Passwords.FORM_SUBMIT_URL, ""); diff --git a/mobile/android/base/tests/BaseTest.java.in b/mobile/android/base/tests/BaseTest.java.in index 158ae1998e6a..bfbc26f758e7 100644 --- a/mobile/android/base/tests/BaseTest.java.in +++ b/mobile/android/base/tests/BaseTest.java.in @@ -6,9 +6,12 @@ import @ANDROID_PACKAGE_NAME@.*; import android.app.Activity; import android.app.Instrumentation; +import android.database.Cursor; +import android.content.ContentValues; import android.content.Intent; import android.os.SystemClock; import android.test.ActivityInstrumentationTestCase2; +import java.io.File; import java.util.HashMap; @@ -25,6 +28,7 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2 { protected String mBaseUrl; private String mTestType; private String mLogFile; + protected String mProfile; static { try { @@ -46,7 +50,8 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2 { // Create the intent to be used with all the important arguments. Intent i = new Intent(Intent.ACTION_MAIN); - i.putExtra("args", "-no-remote -profile " + (String)config.get("profile")); + mProfile = (String)config.get("profile"); + i.putExtra("args", "-no-remote -profile " + mProfile); // Start the activity setActivityIntent(i); @@ -148,4 +153,55 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2 { protected interface BooleanTest { public boolean test(); } + + @SuppressWarnings({"unchecked", "non-varargs"}) + public void SqliteCompare(String dbName, String sqlCommand, ContentValues[] cvs) { + File profile = new File(mProfile); + String dbPath = new File(profile, dbName).getPath(); + + Cursor c = mActions.querySql(dbPath, sqlCommand); + SqliteCompare(c, cvs); + } + + private boolean CursorMatches(Cursor c, String[] columns, ContentValues cv) { + for (int i = 0; i < columns.length; i++) { + String column = columns[i]; + if (cv.containsKey(column)) { + mAsserter.info("Comparing", "Column value " + c.getString(i) + " ?= " + cv.get(column).toString()); + if (!cv.get(column).toString().equals(c.getString(i))) { + return false; + } + } + } + return true; + } + + @SuppressWarnings({"unchecked", "non-varargs"}) + public void SqliteCompare(Cursor c, ContentValues[] cvs) { + mAsserter.is(c.getCount(), cvs.length, "List is correct length"); + if (c.moveToFirst()) { + do { + boolean found = false; + for (int i = 0; !found && i < cvs.length; i++) { + if (CursorMatches(c, cvs[i])) { + found = true; + } + } + mAsserter.is(found, true, "Password was found"); + } while(c.moveToNext()); + } + } + + public boolean CursorMatches(Cursor c, ContentValues cv) { + for (int i = 0; i < c.getColumnCount(); i++) { + String column = c.getColumnName(i); + if (cv.containsKey(column)) { + mAsserter.info("Pass","Column value " + c.getString(i) + " ?= " + cv.get(column).toString()); + if (!cv.get(column).toString().equals(c.getString(i))) { + return false; + } + } + } + return true; + } } diff --git a/mobile/android/base/tests/robocop.ini b/mobile/android/base/tests/robocop.ini index 3f8686626e15..7824b5945d18 100644 --- a/mobile/android/base/tests/robocop.ini +++ b/mobile/android/base/tests/robocop.ini @@ -10,6 +10,8 @@ [testAxisLocking] [testAboutPage] [testWebContentContextMenu] +[testPasswordProvider] +[testFormHistory] # Used for Talos, please don't use in mochitest #[testPan] diff --git a/mobile/android/base/tests/testFormHistory.java.in b/mobile/android/base/tests/testFormHistory.java.in new file mode 100644 index 000000000000..90c9231bbacc --- /dev/null +++ b/mobile/android/base/tests/testFormHistory.java.in @@ -0,0 +1,91 @@ +#filter substitution +package @ANDROID_PACKAGE_NAME@.tests; + +import @ANDROID_PACKAGE_NAME@.*; +import android.app.Activity; +import android.content.ContentValues; +import android.content.ContentResolver; +import android.database.Cursor; +import android.content.Context; +import android.net.Uri; +import java.io.File; +import java.lang.ClassLoader; +import java.util.ArrayList; + +/** + * A basic form history contentprovider test. + * - inserts an element in form history when it is not yet set up + * - inserts an element in form history + * - updates an element in form history + * - deletes an element in form history + */ +public class testFormHistory extends BaseTest { + private static final String DB_NAME = "formhistory.sqlite"; + public void testFormHistory() { + setTestType("mochitest"); + Context context = (Context)getActivity(); + ContentResolver cr = context.getContentResolver(); + ContentValues[] cvs = new ContentValues[1]; + cvs[0] = new ContentValues(); + + Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("Gecko:Ready"); + contentEventExpecter.blockForEvent(); + + Uri formHistoryUri; + try { + ClassLoader classLoader = getActivity().getClassLoader(); + Class fh = classLoader.loadClass("org.mozilla.gecko.db.BrowserContract$FormHistory"); + + cvs[0].put("fieldname", "fieldname"); + cvs[0].put("value", "value"); + cvs[0].put("timesUsed", "0"); + cvs[0].put("guid", "guid"); + + // Attempt to insert into the db + formHistoryUri = (Uri)fh.getField("CONTENT_URI").get(null); + Uri.Builder builder = formHistoryUri.buildUpon(); + formHistoryUri = builder.appendQueryParameter("profilePath", mProfile).build(); + } catch(ClassNotFoundException ex) { + mAsserter.is(false, true, "Error getting class"); + return; + } catch(NoSuchFieldException ex) { + mAsserter.is(false, true, "Error getting field"); + return; + } catch(IllegalAccessException ex) { + mAsserter.is(false, true, "Error using field"); + return; + } + + // Trying to insert should fail the first time round because there is no form history database + // Wait for gecko to reply and then we'll try again + contentEventExpecter = mActions.expectGeckoEvent("FormHistory:Init:Return"); + Uri uri = cr.insert(formHistoryUri, cvs[0]); + mAsserter.is(uri, null, "Insert returned null correctly"); + contentEventExpecter.blockForEvent(); + + uri = cr.insert(formHistoryUri, cvs[0]); + Uri expectedUri = formHistoryUri.buildUpon().appendPath("1").build(); + mAsserter.is(expectedUri.toString(), uri.toString(), "Insert returned correct uri"); + SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs); + + cvs[0].put("fieldname", "fieldname2"); + + int numUpdated = cr.update(formHistoryUri, cvs[0], null, null); + mAsserter.is(1, numUpdated, "Correct number updated"); + SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs); + + int numDeleted = cr.delete(formHistoryUri, null, null); + mAsserter.is(1, numDeleted, "Correct number deleted"); + cvs = new ContentValues[0]; + SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs); + } + + public void tearDown() throws Exception { + super.tearDown(); + + // remove the entire signons.sqlite file + File profile = new File(mProfile); + File db = new File(profile, "formhistory.sqlite"); + db.delete(); + } +} diff --git a/mobile/android/base/tests/testPasswordProvider.java.in b/mobile/android/base/tests/testPasswordProvider.java.in new file mode 100644 index 000000000000..6db2c3279533 --- /dev/null +++ b/mobile/android/base/tests/testPasswordProvider.java.in @@ -0,0 +1,93 @@ +#filter substitution +package @ANDROID_PACKAGE_NAME@.tests; + +import @ANDROID_PACKAGE_NAME@.*; +import android.app.Activity; +import android.content.ContentValues; +import android.content.ContentResolver; +import android.database.Cursor; +import android.content.Context; +import android.net.Uri; +import java.io.File; + +/** + * A basic password contentprovider test. + * - inserts a password when the database is not yet set up + * - inserts a password + * - updates a password + * - deletes a password + */ +public class testPasswordProvider extends BaseTest { + private static final String DB_NAME = "signons.sqlite"; + public void testPasswordProvider() { + setTestType("mochitest"); + Context context = (Context)getActivity(); + ContentResolver cr = context.getContentResolver(); + ContentValues[] cvs = new ContentValues[1]; + cvs[0] = new ContentValues(); + + Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("Gecko:Ready"); + contentEventExpecter.blockForEvent(); + + Uri passwordUri; + try { + ClassLoader classLoader = getActivity().getClassLoader(); + Class pwds = classLoader.loadClass("org.mozilla.gecko.db.BrowserContract$Passwords"); + + cvs[0].put("hostname", "http://www.example.com"); + cvs[0].put("httpRealm", "http://www.example.com"); + cvs[0].put("formSubmitURL", "http://www.example.com"); + cvs[0].put("usernameField", "usernameField"); + cvs[0].put("passwordField", "passwordField"); + cvs[0].put("encryptedUsername", "username"); + cvs[0].put("encryptedPassword", "password"); + cvs[0].put("encType", "0"); + + // Attempt to insert into the db + passwordUri = (Uri)pwds.getField("CONTENT_URI").get(null); + Uri.Builder builder = passwordUri.buildUpon(); + passwordUri = builder.appendQueryParameter("profilePath", mProfile).build(); + } catch(ClassNotFoundException ex) { + mAsserter.is(false, true, "Error getting class"); + return; + } catch(NoSuchFieldException ex) { + mAsserter.is(false, true, "Error getting field"); + return; + } catch(IllegalAccessException ex) { + mAsserter.is(false, true, "Error using field"); + return; + } + // Trying to inset should fail the first time round because there is no pw database + // Wait for gecko to reply and then we'll try again + contentEventExpecter = mActions.expectGeckoEvent("Passwords:Init:Return"); + Uri uri = cr.insert(passwordUri, cvs[0]); + mAsserter.is(uri, null, "Insert returned null correctly"); + contentEventExpecter.blockForEvent(); + + uri = cr.insert(passwordUri, cvs[0]); + SqliteCompare(DB_NAME, "SELECT * FROM moz_logins", cvs); + Uri expectedUri = passwordUri.buildUpon().appendPath("1").build(); + mAsserter.is(expectedUri.toString(), uri.toString(), "Insert returned correct uri"); + + cvs[0].put("usernameField", "usernameField2"); + cvs[0].put("passwordField", "passwordField2"); + + int numUpdated = cr.update(passwordUri, cvs[0], null, null); + mAsserter.is(1, numUpdated, "Correct number updated"); + SqliteCompare(DB_NAME, "SELECT * FROM moz_logins", cvs); + + int numDeleted = cr.delete(passwordUri, null, null); + mAsserter.is(1, numDeleted, "Correct number deleted"); + cvs = new ContentValues[0]; + SqliteCompare(DB_NAME, "SELECT * FROM moz_logins", cvs); + } + + public void tearDown() throws Exception { + super.tearDown(); + + // remove the entire signons.sqlite file + File profile = new File(mProfile); + File db = new File(profile, "signons.sqlite"); + db.delete(); + } +}