Add databases plugin v0 (android) (#441)

Summary:
Adds a plugin for listing the databases, tables and contents of those tables in an android app.
Right now, works with sqlite, but it should be generic enough to work with other db types.

## Changelog

Add initial version of android databases plugin

Creating a PR, I may need to do some cleaning up, but this is to kick off that process.
Pull Request resolved: https://github.com/facebook/flipper/pull/441

Reviewed By: danielbuechele

Differential Revision: D15288831

Pulled By: jknoxville

fbshipit-source-id: 6379ad60d640ea6b0a9473acc03dd6ea81a3a8d4
This commit is contained in:
John Knox
2019-05-28 10:10:31 -07:00
committed by Facebook Github Bot
parent f20a781bca
commit a630b50a8f
27 changed files with 2738 additions and 21 deletions

View File

@@ -0,0 +1,492 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.facebook.flipper.plugins.databases;
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertThat;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.test.core.app.ApplicationProvider;
import com.facebook.flipper.core.FlipperArray;
import com.facebook.flipper.core.FlipperObject;
import com.facebook.flipper.plugins.databases.DatabaseDriver.DatabaseExecuteSqlResponse;
import com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver;
import com.facebook.flipper.plugins.databases.impl.SqliteDatabaseProvider;
import com.facebook.flipper.testing.FlipperConnectionMock;
import com.facebook.flipper.testing.FlipperResponderMock;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class DatabasesFlipperPluginTest {
FlipperConnectionMock connectionMock;
FlipperResponderMock responderMock;
DatabaseHelper databaseHelper1, databaseHelper2;
DatabasesFlipperPlugin plugin;
@Before
public void setUp() {
connectionMock = new FlipperConnectionMock();
responderMock = new FlipperResponderMock();
databaseHelper1 =
new DatabaseHelper(ApplicationProvider.getApplicationContext(), "database1.db");
databaseHelper1
.getWritableDatabase()
.execSQL("INSERT INTO first_table (column1, column2) VALUES('a','b')");
databaseHelper2 =
new DatabaseHelper(ApplicationProvider.getApplicationContext(), "database2.db");
databaseHelper2
.getWritableDatabase()
.execSQL("INSERT INTO first_table (column1, column2) VALUES('a','b')");
plugin =
new DatabasesFlipperPlugin(
new SqliteDatabaseDriver(
ApplicationProvider.getApplicationContext(),
new SqliteDatabaseProvider() {
@Override
public List<File> getDatabaseFiles() {
return Arrays.asList(
ApplicationProvider.getApplicationContext()
.getDatabasePath(databaseHelper1.getDatabaseName()),
ApplicationProvider.getApplicationContext()
.getDatabasePath(databaseHelper2.getDatabaseName()));
}
}));
plugin.onConnect(connectionMock);
}
@After
public void tearDown() {
databaseHelper1.close();
ApplicationProvider.getApplicationContext().deleteDatabase(databaseHelper1.getDatabaseName());
ApplicationProvider.getApplicationContext().deleteDatabase(databaseHelper2.getDatabaseName());
}
@Test
public void testCommandDatabaseList() throws Exception {
// Arrange
// Act
connectionMock.receivers.get("databaseList").onReceive(null, responderMock);
// Assert
assertThat(
responderMock.successes,
hasItem(
new FlipperArray.Builder()
.put(
new FlipperObject.Builder()
.put("id", 1)
.put("name", databaseHelper1.getDatabaseName())
.put(
"tables",
new FlipperArray.Builder()
.put("android_metadata")
.put("first_table")
.put("second_table")
.put("sqlite_sequence")))
.put(
new FlipperObject.Builder()
.put("id", 2)
.put("name", databaseHelper2.getDatabaseName())
.put(
"tables",
new FlipperArray.Builder()
.put("android_metadata")
.put("first_table")
.put("second_table")
.put("sqlite_sequence")))
.build()));
}
@Test
public void testCommandGetTableDataInvalidParams() throws Exception {
// Arrange
// Act
connectionMock
.receivers
.get("getTableData")
.onReceive(
new FlipperObject.Builder()
.put("databaseId", -1) // Wrong id
.put("table", "") // invalid, can't be empty
.build(),
responderMock);
// Assert
assertThat(
responderMock.errors,
hasItem(
new FlipperObject.Builder()
.put("code", DatabasesErrorCodes.ERROR_INVALID_REQUEST)
.put("message", DatabasesErrorCodes.ERROR_INVALID_REQUEST_MESSAGE)
.build()));
}
@Test
public void testCommandGetTableDataInvalidDatabase() throws Exception {
// Arrange
// Act
connectionMock
.receivers
.get("getTableData")
.onReceive(
new FlipperObject.Builder().put("databaseId", 10).put("table", "first_table").build(),
responderMock);
// Assert
assertThat(
responderMock.errors,
hasItem(
new FlipperObject.Builder()
.put("code", DatabasesErrorCodes.ERROR_DATABASE_INVALID)
.put("message", DatabasesErrorCodes.ERROR_DATABASE_INVALID_MESSAGE)
.build()));
}
@Test
public void testCommandGetTableData() throws Exception {
// Arrange
connectionMock.receivers.get("databaseList").onReceive(null, responderMock); // Load data
SQLiteDatabase db = databaseHelper1.getWritableDatabase();
db.execSQL("INSERT INTO first_table (column1, column2) VALUES('c','d')");
db.execSQL("INSERT INTO first_table (column1, column2) VALUES('e','f')");
db.execSQL("INSERT INTO first_table (column1, column2) VALUES('g','h')");
db.close();
// Act
connectionMock
.receivers
.get("getTableData")
.onReceive(
new FlipperObject.Builder()
.put("databaseId", 1)
.put("table", "first_table")
.put("order", "column2")
.put("reverse", true)
.put("start", 1)
.put("count", 2)
.build(),
responderMock);
// Assert
assertThat(
responderMock.successes,
hasItem(
new FlipperObject.Builder()
.put(
"columns",
new FlipperArray.Builder().put("_id").put("column1").put("column2").build())
.put(
"values",
new FlipperArray.Builder()
.put(
new FlipperArray.Builder()
.put(
new FlipperObject.Builder()
.put("type", "integer")
.put("value", 3))
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "e"))
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "f"))
.build())
.put(
new FlipperArray.Builder()
.put(
new FlipperObject.Builder()
.put("type", "integer")
.put("value", 2))
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "c"))
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "d"))
.build())
.build())
.put("start", 1)
.put("count", 2)
.put("total", 4)
.build()));
}
@Test
public void testCommandGetTableStructure() throws Exception {
// Arrange
connectionMock.receivers.get("databaseList").onReceive(null, responderMock); // Load data
SQLiteDatabase db = databaseHelper1.getWritableDatabase();
db.execSQL("CREATE UNIQUE INDEX index_name ON first_table(column1, column2)");
db.close();
// Act
connectionMock
.receivers
.get("getTableStructure")
.onReceive(
new FlipperObject.Builder().put("databaseId", 1).put("table", "first_table").build(),
responderMock);
// Assert
assertThat(
responderMock.successes,
hasItem(
new FlipperObject.Builder()
.put(
"structureColumns",
new FlipperArray.Builder()
.put("column_name")
.put("data_type")
.put("nullable")
.put("default")
.put("primary_key")
.put("foreign_key")
.build())
.put(
"structureValues",
new FlipperArray.Builder()
.put(
new FlipperArray.Builder()
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "_id"))
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "INTEGER"))
.put(
new FlipperObject.Builder()
.put("type", "boolean")
.put("value", true))
.put(new FlipperObject.Builder().put("type", "null"))
.put(
new FlipperObject.Builder()
.put("type", "boolean")
.put("value", true))
.put(new FlipperObject.Builder().put("type", "null"))
.build())
.put(
new FlipperArray.Builder()
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "column1"))
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "TEXT"))
.put(
new FlipperObject.Builder()
.put("type", "boolean")
.put("value", true))
.put(new FlipperObject.Builder().put("type", "null"))
.put(
new FlipperObject.Builder()
.put("type", "boolean")
.put("value", false))
.put(new FlipperObject.Builder().put("type", "null"))
.build())
.put(
new FlipperArray.Builder()
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "column2"))
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "TEXT"))
.put(
new FlipperObject.Builder()
.put("type", "boolean")
.put("value", true))
.put(new FlipperObject.Builder().put("type", "null"))
.put(
new FlipperObject.Builder()
.put("type", "boolean")
.put("value", false))
.put(new FlipperObject.Builder().put("type", "null"))
.build())
.build())
.put(
"indexesColumns",
new FlipperArray.Builder()
.put("index_name")
.put("unique")
.put("indexed_column_name")
.build())
.put(
"indexesValues",
new FlipperArray.Builder()
.put(
new FlipperArray.Builder()
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "index_name"))
.put(
new FlipperObject.Builder()
.put("type", "boolean")
.put("value", true))
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "column1,column2"))
.build())
.build())
.put(
"definition",
"CREATE TABLE first_table (_id INTEGER PRIMARY KEY AUTOINCREMENT,column1 TEXT,column2 TEXT)")
.build()));
}
@Test
public void testCommandExecuteInvalidParams() throws Exception {
// Arrange
// Act
connectionMock
.receivers
.get("execute")
.onReceive(
new FlipperObject.Builder()
.put("databaseId", 1)
.put("value", "") // invalid, can't be empty
.build(),
responderMock);
// Assert
assertThat(
responderMock.errors,
hasItem(
new FlipperObject.Builder()
.put("code", DatabasesErrorCodes.ERROR_INVALID_REQUEST)
.put("message", DatabasesErrorCodes.ERROR_INVALID_REQUEST_MESSAGE)
.build()));
}
@Test
public void testCommandExecuteInvalidDatabase() throws Exception {
// Arrange
// Act
connectionMock
.receivers
.get("execute")
.onReceive(
new FlipperObject.Builder().put("databaseId", 10).put("value", "SELECT...").build(),
responderMock);
// Assert
assertThat(
responderMock.errors,
hasItem(
new FlipperObject.Builder()
.put("code", DatabasesErrorCodes.ERROR_DATABASE_INVALID)
.put("message", DatabasesErrorCodes.ERROR_DATABASE_INVALID_MESSAGE)
.build()));
}
@Test
public void testCommandExecute() throws Exception {
// Arrange
connectionMock.receivers.get("databaseList").onReceive(null, responderMock); // Load data
// Act
connectionMock
.receivers
.get("execute")
.onReceive(
new FlipperObject.Builder()
.put("databaseId", 1)
.put("value", "SELECT column1,column2 FROM first_table")
.build(),
responderMock);
// Assert
assertThat(
responderMock.successes,
hasItem(
new FlipperObject.Builder()
.put("type", DatabaseExecuteSqlResponse.TYPE_SELECT)
.put("columns", new FlipperArray.Builder().put("column1").put("column2").build())
.put(
"values",
new FlipperArray.Builder()
.put(
new FlipperArray.Builder()
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "a"))
.put(
new FlipperObject.Builder()
.put("type", "string")
.put("value", "b"))
.build())
.build())
.build()));
}
public static class DatabaseHelper extends SQLiteOpenHelper {
// If you change the database schema, you must increment the database version.
public static final int DATABASE_VERSION = 1;
private static final String SQL_CREATE_FIRST_TABLE =
"CREATE TABLE first_table ("
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "column1 TEXT,"
+ "column2 TEXT)";
private static final String SQL_CREATE_SECOND_TABLE =
"CREATE TABLE second_table ("
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "column1 TEXT,"
+ "column2 TEXT)";
public DatabaseHelper(Context context, String databaseName) {
super(context, databaseName, null, DATABASE_VERSION);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_FIRST_TABLE);
db.execSQL(SQL_CREATE_SECOND_TABLE);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// This database is only a cache for online data, so its upgrade policy is
// to simply to discard the data and start over
db.execSQL("DROP TABLE IF EXISTS first_table");
db.execSQL("DROP TABLE IF EXISTS second_table");
onCreate(db);
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
}
}