syncbase: Start of client tests.

Tests for naming, creation, deletion, and existence for App, Database,
Table, and Row objects.

Change-Id: I840457414d4be3d6218a10d66d0316b25f7005b4
diff --git a/dart/lib/src/app.dart b/dart/lib/src/app.dart
index 53d254e..f47cfdf 100644
--- a/dart/lib/src/app.dart
+++ b/dart/lib/src/app.dart
@@ -2,7 +2,7 @@
 
 class SyncbaseApp extends NamedResource {
   SyncbaseApp._internal(_proxy, fullName)
-      : super._internal(_proxy, '', fullName);
+      : super._internal(_proxy, null, fullName);
 
   // noSqlDatabase returns the noSqlDatabase with the given relativeName.
   // relativeName must not contain slashes.
diff --git a/dart/lib/src/named_resource.dart b/dart/lib/src/named_resource.dart
index 85f750e..9b51356 100644
--- a/dart/lib/src/named_resource.dart
+++ b/dart/lib/src/named_resource.dart
@@ -2,16 +2,16 @@
 
 // NamedResource is the superclass of resources with names.
 class NamedResource {
-  final String _parentFullName;
   final String fullName;
   final String relativeName;
   final mojom.SyncbaseProxy _proxy;
 
-  NamedResource._internal(_proxy, _parentFullName, relativeName)
+  NamedResource._internal(
+      mojom.SyncbaseProxy _proxy, String _parentFullName, String relativeName)
       : this._proxy = _proxy,
-        this._parentFullName = _parentFullName,
         this.relativeName = relativeName,
-        this.fullName = _parentFullName + '/' + relativeName {
+        this.fullName = (_parentFullName == null ? '' : _parentFullName + '/') +
+            relativeName {
     if (relativeName.contains('/')) {
       throw 'relativeName cannot contain "/": $relativeName';
     }
diff --git a/dart/lib/src/nosql/row.dart b/dart/lib/src/nosql/row.dart
index fb1f034..2968eb6 100644
--- a/dart/lib/src/nosql/row.dart
+++ b/dart/lib/src/nosql/row.dart
@@ -10,13 +10,13 @@
     return v.exists;
   }
 
-  Future<Uint8List> get() async {
+  Future<List<int>> get() async {
     var v = await _proxy.ptr.rowGet(fullName);
     if (isError(v.err)) throw v.err;
     return v.value;
   }
 
-  Future put(Uint8List value) async {
+  Future put(List<int> value) async {
     var v = await _proxy.ptr.rowPut(fullName, value);
     if (isError(v.err)) throw v.err;
     return;
diff --git a/dart/lib/src/nosql/table.dart b/dart/lib/src/nosql/table.dart
index 78ca108..5bdbd0f 100644
--- a/dart/lib/src/nosql/table.dart
+++ b/dart/lib/src/nosql/table.dart
@@ -26,13 +26,13 @@
     return v.exists;
   }
 
-  Future deleteRowRange(Uint8List start, Uint8List limit) async {
+  Future deleteRowRange(List<int> start, List<int> limit) async {
     var v = await _proxy.ptr.tableDeleteRowRange(fullName, start, limit);
     if (isError(v.err)) throw v.err;
     return;
   }
 
-  Stream<mojom.KeyValue> scan(Uint8List start, Uint8List limit) {
+  Stream<mojom.KeyValue> scan(List<int> start, List<int> limit) {
     StreamController<mojom.KeyValue> sc = new StreamController();
     mojom.ScanStream scanStream = new ScanStreamImpl._fromStreamController(sc);
 
diff --git a/dart/lib/syncbase_client.dart b/dart/lib/syncbase_client.dart
index f61c628..3d48c41 100644
--- a/dart/lib/syncbase_client.dart
+++ b/dart/lib/syncbase_client.dart
@@ -1,13 +1,15 @@
 library syncbase_client;
 
 import 'dart:async';
-import 'dart:typed_data' show Uint8List;
 
 import 'package:mojo/application.dart' show Application;
 
 import 'gen/dart-gen/mojom/lib/mojo/syncbase.mojom.dart' as mojom;
 
 // Export struct types from syncbase.mojom.
+// TODO(nlacasse): Create wrapper around Perms, and possibly other struct
+// constructors, since the default constructors are not user-friendly.  They
+// take zero arguments, so all fields must be populated with assignments.
 export 'gen/dart-gen/mojom/lib/mojo/syncbase.mojom.dart'
     show BatchOptions, Perms, SyncGroupMemberInfo, SyncGroupSpec;
 
@@ -22,9 +24,6 @@
   return err != null && err.id != '';
 }
 
-// TODO(nlacasse): Write some tests for this interface now that syncbase runs
-// as a mojo service.  Currently we rely on dartanalyzer for correctness.
-
 class SyncbaseClient {
   final Application _mojoApp;
   final mojom.SyncbaseProxy _proxy;
diff --git a/dart/test/syncbase_app_test.dart b/dart/test/syncbase_app_test.dart
new file mode 100644
index 0000000..97ef1e5
--- /dev/null
+++ b/dart/test/syncbase_app_test.dart
@@ -0,0 +1,29 @@
+library syncbase_app_test;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runAppTests(SyncbaseClient c) {
+  test('getting a handle to an app', () {
+    var appName = utils.uniqueName('app');
+    var app = c.app(appName);
+    expect(app.relativeName, equals(appName));
+    expect(app.fullName, equals(appName));
+  });
+
+  test('creating and deleting an app', () async {
+    var appName = utils.uniqueName('app');
+    var app = c.app(appName);
+
+    expect(await app.exists(), equals(false));
+    await app.create(utils.emptyPerms());
+    expect(await app.exists(), equals(true));
+    await app.delete();
+    expect(await app.exists(), equals(false));
+  });
+
+  // TODO(nlacasse): Test app.get/setPermissions.
+}
diff --git a/dart/test/syncbase_database_test.dart b/dart/test/syncbase_database_test.dart
new file mode 100644
index 0000000..dd81c0f
--- /dev/null
+++ b/dart/test/syncbase_database_test.dart
@@ -0,0 +1,32 @@
+library syncbase_database_test;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runDatabaseTests(SyncbaseClient c) {
+  test('getting a handle to a database', () {
+    var app = c.app(utils.uniqueName('app'));
+    var dbName = utils.uniqueName('db');
+    var db = app.noSqlDatabase(dbName);
+    expect(db.relativeName, equals(dbName));
+    expect(db.fullName, equals(app.fullName + '/' + dbName));
+  });
+
+  test('creating and deleting a database', () async {
+    var app = c.app(utils.uniqueName('app'));
+    await app.create(utils.emptyPerms());
+
+    var db = app.noSqlDatabase(utils.uniqueName('db'));
+
+    expect(await db.exists(), equals(false));
+    await db.create(utils.emptyPerms());
+    expect(await db.exists(), equals(true));
+    await db.delete();
+    expect(await db.exists(), equals(false));
+  });
+
+  // TODO(nlacasse): Test database.get/setPermissions.
+}
diff --git a/dart/test/syncbase_row_test.dart b/dart/test/syncbase_row_test.dart
new file mode 100644
index 0000000..1ce6643
--- /dev/null
+++ b/dart/test/syncbase_row_test.dart
@@ -0,0 +1,51 @@
+library syncbase_row_test;
+
+import 'dart:convert' show UTF8;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runRowTests(SyncbaseClient c) {
+  test('getting a handle to a row', () {
+    var app = c.app(utils.uniqueName('app'));
+    var db = app.noSqlDatabase(utils.uniqueName('db'));
+    var table = db.table(utils.uniqueName('table'));
+
+    var rowName = utils.uniqueName('row');
+    var row = table.row(rowName);
+
+    expect(row.relativeName, equals(rowName));
+    expect(row.fullName, equals(table.fullName + '/' + rowName));
+  });
+
+  test('putting and getting a row', () async {
+    var app = c.app(utils.uniqueName('app'));
+    await app.create(utils.emptyPerms());
+    var db = app.noSqlDatabase(utils.uniqueName('db'));
+    await db.create(utils.emptyPerms());
+    var table = db.table(utils.uniqueName('table'));
+    await table.create(utils.emptyPerms());
+
+    var row = table.row(utils.uniqueName('row'));
+
+    expect(await row.exists(), equals(false));
+
+    var value1 = UTF8.encode("foo");
+    await row.put(value1);
+
+    expect(await row.exists(), equals(true));
+    expect(await row.get(), equals(value1));
+
+    var value2 = UTF8.encode("bar");
+    await row.put(value2);
+
+    expect(await row.exists(), equals(true));
+    expect(await row.get(), equals(value2));
+
+    await row.delete();
+    expect(await row.exists(), equals(false));
+  });
+}
diff --git a/dart/test/syncbase_table_test.dart b/dart/test/syncbase_table_test.dart
new file mode 100644
index 0000000..e0bfd46
--- /dev/null
+++ b/dart/test/syncbase_table_test.dart
@@ -0,0 +1,33 @@
+library syncbase_table_test;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runTableTests(SyncbaseClient c) {
+  test('getting a handle to a table', () {
+    var app = c.app(utils.uniqueName('app'));
+    var db = app.noSqlDatabase(utils.uniqueName('db'));
+    var tableName = utils.uniqueName('table');
+    var table = db.table(tableName);
+    expect(table.relativeName, equals(tableName));
+    expect(table.fullName, equals(db.fullName + '/' + tableName));
+  });
+
+  test('creating and deleting a table', () async {
+    var app = c.app(utils.uniqueName('app'));
+    await app.create(utils.emptyPerms());
+    var db = app.noSqlDatabase(utils.uniqueName('db'));
+    await db.create(utils.emptyPerms());
+
+    var table = db.table(utils.uniqueName('table'));
+
+    expect(await table.exists(), equals(false));
+    await table.create(utils.emptyPerms());
+    expect(await table.exists(), equals(true));
+    await table.delete();
+    expect(await table.exists(), equals(false));
+  });
+}
diff --git a/dart/test/syncbase_test.dart b/dart/test/syncbase_test.dart
index 200c9d3..1a1f628 100755
--- a/dart/test/syncbase_test.dart
+++ b/dart/test/syncbase_test.dart
@@ -7,6 +7,12 @@
 import 'package:ether/initialized_application.dart' show InitializedApplication;
 import 'package:ether/syncbase_client.dart' show SyncbaseClient;
 
+// Import other test files.
+import './syncbase_app_test.dart' show runAppTests;
+import './syncbase_database_test.dart' show runDatabaseTests;
+import './syncbase_row_test.dart' show runRowTests;
+import './syncbase_table_test.dart' show runTableTests;
+
 main(List args) async {
   InitializedApplication app = new InitializedApplication.fromHandle(args[0]);
   await app.initialized;
@@ -23,9 +29,11 @@
     app.resetConnections();
   });
 
-  test('app(foo).exists() should be false', () {
-    expect(c.app('foo').exists(), completion(isFalse));
-  });
+  // Run imported tests.
+  runAppTests(c);
+  runDatabaseTests(c);
+  runTableTests(c);
+  runRowTests(c);
 
   // Append a final test to terminate shell connection.
   // TODO(nlacasse): Remove this once package 'test' supports a global tearDown
diff --git a/dart/test/utils.dart b/dart/test/utils.dart
new file mode 100644
index 0000000..37c4e8a
--- /dev/null
+++ b/dart/test/utils.dart
@@ -0,0 +1,18 @@
+library utils;
+
+import 'package:ether/syncbase_client.dart' show Perms;
+
+// Returns an empty Perms object.
+Perms emptyPerms() => new Perms()..json = '{}';
+
+// Returns the current timestamp in ms since epoch.
+int timestamp() => new DateTime.now().millisecondsSinceEpoch;
+
+int _nameCounter = timestamp();
+
+// Returns a new unique name.
+String uniqueName(String type) {
+  type ??= 'unknown';
+  _nameCounter++;
+  return type + '-' + _nameCounter.toString();
+}