swift: Refactor login for new configure/login flow.

Updates the configure/login flow to match the updates to the CGO API,
which fixes the blessings issue that was preventing unit tests in
from passing. Also has a few other bug fixes listed below:

- Checks if we're logged in at configure, and if so then starts serving.
- Refactors post-login logic (creating default db, create-and-join, etc)
  to a central function that is only called once we have blessings.
- Makes sure the HLAPI's queue and LLAPI's queue are the same.
- Updates unit tests in SyncbaseCore to use new login flow.
- Makes database/collection/syncgroup adopt CustomStringConvertible
- Filter out internal userdata_syncgroup from listed collections
- Rename erroneous syncbaseId to syncgroupId

Change-Id: I60976427db035432dddae6015cfcbfdfe4f6b72a
diff --git a/Syncbase/Source/Collection.swift b/Syncbase/Source/Collection.swift
index 96f496f..98ea5db 100644
--- a/Syncbase/Source/Collection.swift
+++ b/Syncbase/Source/Collection.swift
@@ -7,7 +7,7 @@
 
 /// Represents an ordered set of key-value pairs.
 /// To get a Collection handle, call `Database.collection`.
-public class Collection {
+public class Collection: CustomStringConvertible {
   let coreCollection: SyncbaseCore.Collection
   let databaseHandle: DatabaseHandle
 
@@ -98,4 +98,8 @@
       try (databaseHandle as! Database).runInBatch(op: op)
     }
   }
+
+  public var description: String {
+    return "[Syncbase.Collection id=\(collectionId)]"
+  }
 }
diff --git a/Syncbase/Source/Database.swift b/Syncbase/Source/Database.swift
index 9f6436f..4a456e0 100644
--- a/Syncbase/Source/Database.swift
+++ b/Syncbase/Source/Database.swift
@@ -79,7 +79,7 @@
 
 /// A set of collections and syncgroups.
 /// To get a Database handle, call `Syncbase.database`.
-public class Database: DatabaseHandle {
+public class Database: DatabaseHandle, CustomStringConvertible {
   let coreDatabase: SyncbaseCore.Database
 
   // These are all static because we might have active handlers to a database while it goes
@@ -135,9 +135,11 @@
   /// Returns all collections in the database.
   public func collections() throws -> [Collection] {
     return try SyncbaseError.wrap {
-      let coreIds = try self.coreDatabase.listCollections()
+      let coreIds = try self.coreDatabase.listCollections().filter({ return $0.name != Syncbase.USERDATA_SYNCGROUP_NAME })
       return try coreIds.map { coreId in
-        return Collection(coreCollection: try self.coreDatabase.collection(coreId), databaseHandle: self)
+        return Collection(
+          coreCollection: try self.coreDatabase.collection(coreId),
+          databaseHandle: self)
       }
     }
   }
@@ -404,4 +406,8 @@
       }
     }
   }
+
+  public var description: String {
+    return "[Syncbase.Database id=\(databaseId)]"
+  }
 }
diff --git a/Syncbase/Source/Error.swift b/Syncbase/Source/Error.swift
index 94216a9..cee530b 100644
--- a/Syncbase/Source/Error.swift
+++ b/Syncbase/Source/Error.swift
@@ -9,7 +9,6 @@
   case AlreadyConfigured
   case NotConfigured
   case NotLoggedIn
-  case IllegalArgument(detail: String)
   case BatchError(detail: String)
   case BlessingError(detail: String)
   case UnknownError(err: ErrorType)
@@ -31,6 +30,7 @@
   case InvalidOperation(reason: String)
   case InvalidUTF8(invalidUtf8: String)
   case CastError(obj: Any)
+  case IllegalArgument(detail: String)
 
   init?(coreError: SyncbaseCore.SyncbaseError) {
     switch coreError {
@@ -54,6 +54,7 @@
     case .InvalidOperation(let reason): self = .InvalidOperation(reason: reason)
     case .InvalidUTF8(let invalidUtf8): self = .InvalidUTF8(invalidUtf8: invalidUtf8)
     case .CastError(let obj): self = .CastError(obj: obj)
+    case .IllegalArgument(let detail): self = .IllegalArgument(detail: detail)
     }
   }
 
@@ -71,7 +72,6 @@
 extension SyncbaseError: CustomStringConvertible {
   public var description: String {
     switch self {
-    case .IllegalArgument(let detail): return "Illegal argument: \(detail)"
     case .BatchError(let detail): return "Batch error: \(detail)"
     case .BlessingError(let detail): return "Blessing error: \(detail)"
     case .UnknownError(let err): return "Unknown error: \(err)"
@@ -97,6 +97,7 @@
     case .InvalidOperation(let reason): return "Invalid operation: \(reason)"
     case .InvalidUTF8(let invalidUtf8): return "Unable to convert to utf8: \(invalidUtf8)"
     case .CastError(let obj): return "Unable to convert to cast: \(obj)"
+    case .IllegalArgument(let detail): return "Illegal argument: \(detail)"
     }
   }
 }
diff --git a/Syncbase/Source/Syncbase.swift b/Syncbase/Source/Syncbase.swift
index 2ec27d4..7d02ad2 100644
--- a/Syncbase/Source/Syncbase.swift
+++ b/Syncbase/Source/Syncbase.swift
@@ -14,8 +14,8 @@
     DB_NAME = "db",
     USERDATA_SYNCGROUP_NAME = "userdata__"
   // Initialization state
-  static var didCreateOrJoin = false
   static var didInit = false
+  static var didPostLogin = false
   // Main database.
   static var db: Database?
   // Options for opening a database.
@@ -26,7 +26,15 @@
   static var mountPoints = ["/ns.dev.v.io:8101/tmp/todos/users/"]
   static var rootDir = Syncbase.defaultRootDir
   /// Queue used to dispatch all asynchronous callbacks. Defaults to main.
-  public static var queue: dispatch_queue_t = dispatch_get_main_queue()
+  public static var queue: dispatch_queue_t {
+    // Map directly to SyncbaseCore.
+    get {
+      return SyncbaseCore.Syncbase.queue
+    }
+    set(queue) {
+      SyncbaseCore.Syncbase.queue = queue
+    }
+  }
 
   static public var defaultRootDir: String {
     return NSFileManager.defaultManager()
@@ -73,7 +81,7 @@
     disableSyncgroupPublishing: Bool = false,
     disableUserdataSyncgroup: Bool = false,
     queue: dispatch_queue_t = dispatch_get_main_queue()) throws {
-      if didInit {
+      if Syncbase.didInit {
         throw SyncbaseError.AlreadyConfigured
       }
       Syncbase.adminUserId = adminUserId
@@ -82,56 +90,50 @@
       Syncbase.defaultBlessingStringPrefix = defaultBlessingStringPrefix
       Syncbase.disableSyncgroupPublishing = disableSyncgroupPublishing
       Syncbase.disableUserdataSyncgroup = disableUserdataSyncgroup
-
-      // TODO(zinman): Reconfigure this logic once we have CL #23295 merged.
-      let database = try Syncbase.startSyncbaseAndInitDatabase()
-      if (Syncbase.disableUserdataSyncgroup) {
-        try database.collection(Syncbase.USERDATA_SYNCGROUP_NAME, withoutSyncgroup: true)
-      } else {
-        // This gets deferred to login as it's blocking. Once we've logged in we don't need it
-        // anyway.
-        didCreateOrJoin = false
-        // FIXME(zinman): Implement create-or-join (and watch) of userdata syncgroup.
-        throw SyncbaseError.IllegalArgument(detail: "Synced userdata collection is not yet supported")
+      Syncbase.didPostLogin = false
+      // We don't need to set Syncbase.queue as it is a proxy for SyncbaseCore's queue, which is
+      // set in the configure below.
+      try SyncbaseError.wrap {
+        try SyncbaseCore.Syncbase.configure(rootDir: Syncbase.rootDir, queue: queue)
       }
-      Syncbase.db = database
+      // We use SyncbaseCore's isLoggedIn because this frameworks would fail as didInit hasn't
+      // been set to true yet.
+      if (SyncbaseCore.Syncbase.isLoggedIn) {
+        do {
+          try Syncbase.postLoginCreateDefaults()
+        } catch let e {
+          // If we get an exception after configuring the low-level API, make sure we shutdown
+          // Syncbase so that any subsequent call to this configure method doesn't get a
+          // SyncbaseError.AlreadyConfigured exception from SyncbaseCore.Syncbase.configure.
+          SyncbaseCore.Syncbase.shutdown()
+          throw e
+        }
+      }
       Syncbase.didInit = true
   }
 
-  private static func startSyncbaseAndInitDatabase() throws -> Database {
-    if Syncbase.rootDir == "" {
-      throw SyncbaseError.IllegalArgument(detail: "Missing rootDir")
+  private static func postLoginCreateDefaults() throws {
+    let coreDb = try SyncbaseCore.Syncbase.database(Syncbase.DB_NAME)
+    let database = Database(coreDatabase: coreDb)
+    try database.createIfMissing()
+    if (Syncbase.disableUserdataSyncgroup) {
+      try database.collection(Syncbase.USERDATA_SYNCGROUP_NAME, withoutSyncgroup: true)
+    } else {
+      // FIXME(zinman): Implement create-or-join (and watch) of userdata syncgroup.
+      throw SyncbaseError.IllegalArgument(detail: "Synced userdata collection is not yet supported")
     }
-    if !NSFileManager.defaultManager().fileExistsAtPath(Syncbase.rootDir) {
-      try NSFileManager.defaultManager().createDirectoryAtPath(
-        Syncbase.rootDir,
-        withIntermediateDirectories: true,
-        attributes: [NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication])
-    }
-    return try SyncbaseError.wrap {
-      // TODO(zinman): Verify we should be using the user's blessing (by not explicitly passing
-      // the blessings in an Identifier).
-      try SyncbaseCore.Syncbase.configure(rootDir: Syncbase.rootDir, queue: Syncbase.queue)
-      let coreDb = try SyncbaseCore.Syncbase.database(Syncbase.DB_NAME)
-      let res = Database(coreDatabase: coreDb)
-      try res.createIfMissing()
-      return res
-    }
+    Syncbase.db = database
+    Syncbase.didPostLogin = true
   }
 
   /// Returns the shared database handle. Must have already called `configure` and be logged in,
   /// otherwise this will throw a `SyncbaseError.NotConfigured` or `SyncbaseError.NotLoggedIn`
   /// error.
   public static func database() throws -> Database {
-    guard let db = Syncbase.db where Syncbase.didInit else {
+    if !Syncbase.didInit {
       throw SyncbaseError.NotConfigured
     }
-    if !SyncbaseCore.Syncbase.isLoggedIn {
-      throw SyncbaseError.NotLoggedIn
-    }
-    if !Syncbase.disableUserdataSyncgroup && !Syncbase.didCreateOrJoin {
-      // Create-or-join of userdata syncgroup occurs in login. We must have failed between
-      // login() and the create-or-join call.
+    guard let db = Syncbase.db where Syncbase.didPostLogin else {
       throw SyncbaseError.NotLoggedIn
     }
     return db
@@ -150,7 +152,7 @@
     SyncbaseCore.Syncbase.login(
       SyncbaseCore.GoogleOAuthCredentials(token: credentials.token),
       callback: { err in
-        guard err != nil else {
+        guard err == nil else {
           if let e = err as? SyncbaseCore.SyncbaseError {
             callback(err: SyncbaseError(coreError: e))
           } else {
@@ -158,21 +160,17 @@
           }
           return
         }
-        if Syncbase.disableUserdataSyncgroup {
-          // Success
-          dispatch_async(Syncbase.queue) {
-            callback(err: nil)
+        // postLoginCreateDefaults can be blocking when performing create-or-join. Run on
+        // a background queue to prevent blocking from the Go callback.
+        dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
+          var callbackErr: ErrorType?
+          do {
+            try postLoginCreateDefaults()
+          } catch let e {
+            callbackErr = e
           }
-        } else {
-          dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
-            callback(err: SyncbaseError.IllegalArgument(detail:
-                "Synced userdata collection is not yet supported"))
-            return
-            // FIXME(zinman): Implement create-or-join (and watch) of userdata syncgroup.
-            // Syncbase.didCreateOrJoin = true
-            // dispatch_async(Syncbase.queue) {
-            // callback(nil)
-            // }
+          dispatch_async(Syncbase.queue) {
+            callback(err: callbackErr)
           }
         }
     })
@@ -182,11 +180,6 @@
     if !Syncbase.didInit {
       throw SyncbaseError.NotConfigured
     }
-    if !Syncbase.disableUserdataSyncgroup && !Syncbase.didCreateOrJoin {
-      // Create-or-join of userdata syncgroup occurs in login. We must have failed between
-      // login() and the create-or-join call.
-      throw SyncbaseError.NotLoggedIn
-    }
     return SyncbaseCore.Syncbase.isLoggedIn
   }
 }
diff --git a/Syncbase/Source/Syncgroup.swift b/Syncbase/Source/Syncgroup.swift
index 7645945..e27065e 100644
--- a/Syncbase/Source/Syncgroup.swift
+++ b/Syncbase/Source/Syncgroup.swift
@@ -7,7 +7,7 @@
 
 /// Represents a set of collections, synced amongst a set of users.
 /// To get a Syncgroup handle, call `Database.syncgroup`.
-public class Syncgroup {
+public class Syncgroup: CustomStringConvertible {
   let database: Database
   let coreSyncgroup: SyncbaseCore.Syncgroup
 
@@ -42,7 +42,7 @@
   }
 
   /// Returns the id of this syncgroup.
-  public var syncbaseId: Identifier {
+  public var syncgroupId: Identifier {
     return Identifier(coreId: coreSyncgroup.syncgroupId)
   }
 
@@ -125,4 +125,8 @@
       }
     }
   }
+
+  public var description: String {
+    return "[Syncbase.Syncgroup id=\(syncgroupId)]"
+  }
 }
diff --git a/Syncbase/Tests/BasicDatabaseTests.swift b/Syncbase/Tests/BasicDatabaseTests.swift
index 7a8ed1f..2d90846 100644
--- a/Syncbase/Tests/BasicDatabaseTests.swift
+++ b/Syncbase/Tests/BasicDatabaseTests.swift
@@ -4,12 +4,31 @@
 
 import XCTest
 @testable import Syncbase
+import enum Syncbase.Syncbase
 @testable import SyncbaseCore
 
+let testQueue = dispatch_queue_create("SyncbaseQueue", DISPATCH_QUEUE_SERIAL)
+
 class BasicDatabaseTests: XCTestCase {
   override class func setUp() {
     SyncbaseCore.Syncbase.isUnitTest = true
-    try! Syncbase.configure(adminUserId: "unittest@google.com")
+    let rootDir = NSFileManager.defaultManager()
+      .URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]
+      .URLByAppendingPathComponent("SyncbaseUnitTest")
+      .absoluteString
+    // TODO(zinman): Once we have create-and-join implemented don't always set
+    // disableUserdataSyncgroup to true.
+    try! Syncbase.configure(
+      adminUserId: "unittest@google.com",
+      rootDir: rootDir,
+      disableUserdataSyncgroup: true,
+      queue: testQueue)
+    let semaphore = dispatch_semaphore_create(0)
+    Syncbase.login(GoogleOAuthCredentials(token: ""), callback: { err in
+      XCTAssertNil(err)
+      dispatch_semaphore_signal(semaphore)
+    })
+    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
   }
 
   override class func tearDown() {
diff --git a/SyncbaseCore/Source/Errors.swift b/SyncbaseCore/Source/Errors.swift
index 925a7e1..c434d73 100644
--- a/SyncbaseCore/Source/Errors.swift
+++ b/SyncbaseCore/Source/Errors.swift
@@ -25,6 +25,7 @@
   case InvalidOperation(reason: String)
   case InvalidUTF8(invalidUtf8: String)
   case CastError(obj: Any)
+  case IllegalArgument(detail: String)
 
   init?(_ err: VError) {
     // TODO(zinman): Make VError better by having the proper arguments transmitted across
@@ -70,6 +71,7 @@
     case .InvalidOperation(let reason): return "Invalid operation: \(reason)"
     case .InvalidUTF8(let invalidUtf8): return "Unable to convert to UTF-8: \(invalidUtf8)"
     case .CastError(let obj): return "Unable to convert to cast: \(obj)"
+    case .IllegalArgument(let detail): return "Illegal argument: \(detail)"
     }
   }
 }
diff --git a/SyncbaseCore/Source/Principal.swift b/SyncbaseCore/Source/Principal.swift
index 70cc3f9..f37787a 100644
--- a/SyncbaseCore/Source/Principal.swift
+++ b/SyncbaseCore/Source/Principal.swift
@@ -41,14 +41,4 @@
     v23_syncbase_BlessingStoreDebugString(&cStr)
     return cStr.toString() ?? "ERROR"
   }
-
-  /// True if the blessings have been successfully retrieved via exchanging an oauth token.
-  static func blessingsAreValid() -> Bool {
-    do {
-      try userBlessing()
-      return true
-    } catch {
-      return false
-    }
-  }
 }
diff --git a/SyncbaseCore/Source/Syncbase.swift b/SyncbaseCore/Source/Syncbase.swift
index 080ffc6..bbdbfcf 100644
--- a/SyncbaseCore/Source/Syncbase.swift
+++ b/SyncbaseCore/Source/Syncbase.swift
@@ -8,9 +8,8 @@
 public enum Syncbase {
   /// The dispatch queue to run callbacks on. Defaults to main.
   public static var queue: dispatch_queue_t = dispatch_get_main_queue()
-
+  // Internal variables
   static var didInit = false
-
   static var isUnitTest = false
 
   public static func configure(
@@ -23,13 +22,33 @@
       if didInit {
         throw SyncbaseError.AlreadyConfigured
       }
+      if rootDir == "" {
+        throw SyncbaseError.IllegalArgument(detail: "Missing rootDir")
+      }
+      if !NSFileManager.defaultManager().fileExistsAtPath(rootDir) {
+        try NSFileManager.defaultManager().createDirectoryAtPath(
+          rootDir,
+          withIntermediateDirectories: true,
+          attributes: [NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication])
+      }
       v23_syncbase_Init(
         v23_syncbase_Bool(false),
         try rootDir.toCgoString(),
         v23_syncbase_Bool(isUnitTest))
+      if isLoggedIn {
+        try serve()
+      }
+      Syncbase.queue = queue
       Syncbase.didInit = true
   }
 
+  /// Starts serving Syncbase post-initialization and login. Internal use only.
+  static func serve() throws {
+    try VError.maybeThrow { errPtr in
+      v23_syncbase_Serve(errPtr)
+    }
+  }
+
   /// Shuts down the Syncbase service. You must call configure again before any calls will work.
   public static func shutdown() {
     v23_syncbase_Shutdown()
@@ -68,8 +87,9 @@
   /// Must return true before any Syncbase operation can work. Authorize using GoogleCredentials
   /// created from a Google OAuth token (you should use the Google Sign In SDK to get this).
   public static var isLoggedIn: Bool {
-    log.debug("Blessings debug string is \(Principal.blessingsDebugDescription)")
-    return Principal.blessingsAreValid()
+    var ret = v23_syncbase_Bool(false)
+    v23_syncbase_IsLoggedIn(&ret)
+    return ret.toBool()
   }
 
   /// For debugging the current Syncbase user blessings.
diff --git a/SyncbaseCore/Tests/BasicDatabaseTests.swift b/SyncbaseCore/Tests/BasicDatabaseTests.swift
index b59ad1a..1ae4913 100644
--- a/SyncbaseCore/Tests/BasicDatabaseTests.swift
+++ b/SyncbaseCore/Tests/BasicDatabaseTests.swift
@@ -6,6 +6,13 @@
 import XCTest
 @testable import SyncbaseCore
 
+struct EmptyCredentials: OAuthCredentials {
+  let provider: OAuthProvider = OAuthProvider.Google
+  let token: String = ""
+}
+
+let testQueue = dispatch_queue_create("SyncbaseQueue", DISPATCH_QUEUE_SERIAL)
+
 class BasicDatabaseTests: XCTestCase {
   override class func setUp() {
     Syncbase.isUnitTest = true
@@ -13,7 +20,13 @@
       .URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]
       .URLByAppendingPathComponent("SyncbaseUnitTest")
       .absoluteString
-    try! Syncbase.configure(rootDir: rootDir)
+    try! Syncbase.configure(rootDir: rootDir, queue: testQueue)
+    let semaphore = dispatch_semaphore_create(0)
+    Syncbase.login(EmptyCredentials(), callback: { err in
+      XCTAssertNil(err)
+      dispatch_semaphore_signal(semaphore)
+    })
+    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
   }
 
   override class func tearDown() {