Merge pull request #3818 from stanley-cheung/jenkins_interop_script_more_flexibility

Allow extra docker args to be passed to build_interop_image.sh
diff --git a/binding.gyp b/binding.gyp
index 39036b3..ba7b6df 100644
--- a/binding.gyp
+++ b/binding.gyp
@@ -54,6 +54,24 @@
     'include_dirs': [
       '.',
       'include'
+    ],
+    'conditions': [
+      ['OS != "win"', {
+        'conditions': [
+          ['config=="gcov"', {
+            'cflags': [
+              '-ftest-coverage',
+              '-fprofile-arcs',
+              '-O0'
+            ],
+            'ldflags': [
+              '-ftest-coverage',
+              '-fprofile-arcs'
+            ]
+          }
+         ]
+        ]
+      }],
     ]
   },
   'targets': [
@@ -279,22 +297,6 @@
         '-g'
       ],
       "conditions": [
-        ['OS != "win"', {
-          'conditions': [
-            ['config=="gcov"', {
-              'cflags': [
-                '-ftest-coverage',
-                '-fprofile-arcs',
-                '-O0'
-              ],
-              'ldflags': [
-                '-ftest-coverage',
-                '-fprofile-arcs'
-              ]
-            }
-           ]
-          ]
-        }],
         ['OS == "mac"', {
           'xcode_settings': {
             'MACOSX_DEPLOYMENT_TARGET': '10.9',
diff --git a/src/node/ext/call.cc b/src/node/ext/call.cc
index b63e294..fe11905 100644
--- a/src/node/ext/call.cc
+++ b/src/node/ext/call.cc
@@ -83,6 +83,18 @@
 Callback *Call::constructor;
 Persistent<FunctionTemplate> Call::fun_tpl;
 
+/**
+ * Helper function for throwing errors with a grpc_call_error value.
+ * Modified from the answer by Gus Goose to
+ * http://stackoverflow.com/questions/31794200.
+ */
+Local<Value> nanErrorWithCode(const char *msg, grpc_call_error code) {
+  EscapableHandleScope scope;
+  Local<Object> err = Nan::Error(msg).As<Object>();
+  Nan::Set(err, Nan::New("code").ToLocalChecked(), Nan::New<Uint32>(code));
+  return scope.Escape(err);
+}
+
 bool EndsWith(const char *str, const char *substr) {
   return strcmp(str+strlen(str)-strlen(substr), substr) == 0;
 }
@@ -712,7 +724,11 @@
   Call *call = ObjectWrap::Unwrap<Call>(info.This());
   grpc_status_code code = static_cast<grpc_status_code>(
       Nan::To<uint32_t>(info[0]).FromJust());
-  Utf8String details(info[0]);
+  if (code == GRPC_STATUS_OK) {
+    return Nan::ThrowRangeError(
+        "cancelWithStatus cannot be called with OK status");
+  }
+  Utf8String details(info[1]);
   grpc_call_cancel_with_status(call->wrapped_call, code, *details, NULL);
 }
 
diff --git a/src/node/ext/call.h b/src/node/ext/call.h
index dd6c38e..1e3c3ba 100644
--- a/src/node/ext/call.h
+++ b/src/node/ext/call.h
@@ -53,18 +53,7 @@
 
 typedef Nan::Persistent<v8::Value, Nan::CopyablePersistentTraits<v8::Value>> PersistentValue;
 
-/**
- * Helper function for throwing errors with a grpc_call_error value.
- * Modified from the answer by Gus Goose to
- * http://stackoverflow.com/questions/31794200.
- */
-inline v8::Local<v8::Value> nanErrorWithCode(const char *msg,
-                                             grpc_call_error code) {
-  Nan::EscapableHandleScope scope;
-    v8::Local<v8::Object> err = Nan::Error(msg).As<v8::Object>();
-    Nan::Set(err, Nan::New("code").ToLocalChecked(), Nan::New<v8::Uint32>(code));
-    return scope.Escape(err);
-}
+v8::Local<v8::Value> nanErrorWithCode(const char *msg, grpc_call_error code);
 
 v8::Local<v8::Value> ParseMetadata(const grpc_metadata_array *metadata_array);
 
diff --git a/src/node/ext/call_credentials.cc b/src/node/ext/call_credentials.cc
index 839bb56..ff16a1f 100644
--- a/src/node/ext/call_credentials.cc
+++ b/src/node/ext/call_credentials.cc
@@ -126,16 +126,9 @@
     info.GetReturnValue().Set(info.This());
     return;
   } else {
-    const int argc = 1;
-    Local<Value> argv[argc] = {info[0]};
-    MaybeLocal<Object> maybe_instance = constructor->GetFunction()->NewInstance(
-        argc, argv);
-    if (maybe_instance.IsEmpty()) {
-      // There's probably a pending exception
-      return;
-    } else {
-      info.GetReturnValue().Set(maybe_instance.ToLocalChecked());
-    }
+    // This should never be called directly
+    return Nan::ThrowTypeError(
+        "CallCredentials can only be created with the provided functions");
   }
 }
 
diff --git a/src/node/ext/channel.cc b/src/node/ext/channel.cc
index a328c01..584a0cf 100644
--- a/src/node/ext/channel.cc
+++ b/src/node/ext/channel.cc
@@ -71,6 +71,72 @@
 Callback *Channel::constructor;
 Persistent<FunctionTemplate> Channel::fun_tpl;
 
+bool ParseChannelArgs(Local<Value> args_val,
+                      grpc_channel_args **channel_args_ptr) {
+  if (args_val->IsUndefined() || args_val->IsNull()) {
+    *channel_args_ptr = NULL;
+    return true;
+  }
+  if (!args_val->IsObject()) {
+    *channel_args_ptr = NULL;
+    return false;
+  }
+  grpc_channel_args *channel_args = reinterpret_cast<grpc_channel_args*>(
+      malloc(sizeof(channel_args)));
+  *channel_args_ptr = channel_args;
+  Local<Object> args_hash = Nan::To<Object>(args_val).ToLocalChecked();
+  Local<Array> keys = Nan::GetOwnPropertyNames(args_hash).ToLocalChecked();
+  channel_args->num_args = keys->Length();
+  channel_args->args = reinterpret_cast<grpc_arg *>(
+      calloc(channel_args->num_args, sizeof(grpc_arg)));
+  for (unsigned int i = 0; i < channel_args->num_args; i++) {
+    Local<Value> key = Nan::Get(keys, i).ToLocalChecked();
+    Utf8String key_str(key);
+    if (*key_str == NULL) {
+      // Key string onversion failed
+      return false;
+    }
+    Local<Value> value = Nan::Get(args_hash, key).ToLocalChecked();
+    if (value->IsInt32()) {
+      channel_args->args[i].type = GRPC_ARG_INTEGER;
+      channel_args->args[i].value.integer = Nan::To<int32_t>(value).FromJust();
+    } else if (value->IsString()) {
+      Utf8String val_str(value);
+      channel_args->args[i].type = GRPC_ARG_STRING;
+      channel_args->args[i].value.string = reinterpret_cast<char*>(
+          calloc(val_str.length() + 1,sizeof(char)));
+      memcpy(channel_args->args[i].value.string,
+             *val_str, val_str.length() + 1);
+    } else {
+      // The value does not match either of the accepted types
+      return false;
+    }
+    channel_args->args[i].key = reinterpret_cast<char*>(
+        calloc(key_str.length() + 1, sizeof(char)));
+    memcpy(channel_args->args[i].key, *key_str, key_str.length() + 1);
+  }
+  return true;
+}
+
+void DeallocateChannelArgs(grpc_channel_args *channel_args) {
+  if (channel_args == NULL) {
+    return;
+  }
+  for (size_t i = 0; i < channel_args->num_args; i++) {
+    if (channel_args->args[i].key == NULL) {
+      /* NULL key implies that this argument and all subsequent arguments failed
+       * to parse */
+      break;
+    }
+    free(channel_args->args[i].key);
+    if (channel_args->args[i].type == GRPC_ARG_STRING) {
+      free(channel_args->args[i].value.string);
+    }
+  }
+  free(channel_args->args);
+  free(channel_args);
+}
+
 Channel::Channel(grpc_channel *channel) : wrapped_channel(channel) {}
 
 Channel::~Channel() {
@@ -119,49 +185,11 @@
     ChannelCredentials *creds_object = ObjectWrap::Unwrap<ChannelCredentials>(
         Nan::To<Object>(info[1]).ToLocalChecked());
     creds = creds_object->GetWrappedCredentials();
-    grpc_channel_args *channel_args_ptr;
-    if (info[2]->IsUndefined()) {
-      channel_args_ptr = NULL;
-      wrapped_channel = grpc_insecure_channel_create(*host, NULL, NULL);
-    } else if (info[2]->IsObject()) {
-      Local<Object> args_hash = Nan::To<Object>(info[2]).ToLocalChecked();
-      Local<Array> keys(Nan::GetOwnPropertyNames(args_hash).ToLocalChecked());
-      grpc_channel_args channel_args;
-      channel_args.num_args = keys->Length();
-      channel_args.args = reinterpret_cast<grpc_arg *>(
-          calloc(channel_args.num_args, sizeof(grpc_arg)));
-      /* These are used to keep all strings until then end of the block, then
-         destroy them */
-      std::vector<Nan::Utf8String *> key_strings(keys->Length());
-      std::vector<Nan::Utf8String *> value_strings(keys->Length());
-      for (unsigned int i = 0; i < channel_args.num_args; i++) {
-        MaybeLocal<String> maybe_key = Nan::To<String>(
-            Nan::Get(keys, i).ToLocalChecked());
-        if (maybe_key.IsEmpty()) {
-          free(channel_args.args);
-          return Nan::ThrowTypeError("Arg keys must be strings");
-        }
-        Local<String> current_key = maybe_key.ToLocalChecked();
-        Local<Value> current_value = Nan::Get(args_hash,
-                                               current_key).ToLocalChecked();
-        key_strings[i] = new Nan::Utf8String(current_key);
-        channel_args.args[i].key = **key_strings[i];
-        if (current_value->IsInt32()) {
-          channel_args.args[i].type = GRPC_ARG_INTEGER;
-          channel_args.args[i].value.integer = Nan::To<int32_t>(
-              current_value).FromJust();
-        } else if (current_value->IsString()) {
-          channel_args.args[i].type = GRPC_ARG_STRING;
-          value_strings[i] = new Nan::Utf8String(current_value);
-          channel_args.args[i].value.string = **value_strings[i];
-        } else {
-          free(channel_args.args);
-          return Nan::ThrowTypeError("Arg values must be strings");
-        }
-      }
-      channel_args_ptr = &channel_args;
-    } else {
-      return Nan::ThrowTypeError("Channel expects a string and an object");
+    grpc_channel_args *channel_args_ptr = NULL;
+    if (!ParseChannelArgs(info[2], &channel_args_ptr)) {
+      DeallocateChannelArgs(channel_args_ptr);
+      return Nan::ThrowTypeError("Channel options must be an object with "
+                                 "string keys and integer or string values");
     }
     if (creds == NULL) {
       wrapped_channel = grpc_insecure_channel_create(*host, channel_args_ptr,
@@ -170,9 +198,7 @@
       wrapped_channel =
           grpc_secure_channel_create(creds, *host, channel_args_ptr, NULL);
     }
-    if (channel_args_ptr != NULL) {
-      free(channel_args_ptr->args);
-    }
+    DeallocateChannelArgs(channel_args_ptr);
     Channel *channel = new Channel(wrapped_channel);
     channel->Wrap(info.This());
     info.GetReturnValue().Set(info.This());
diff --git a/src/node/ext/channel.h b/src/node/ext/channel.h
index 0062fd0..9ec28e1 100644
--- a/src/node/ext/channel.h
+++ b/src/node/ext/channel.h
@@ -41,6 +41,11 @@
 namespace grpc {
 namespace node {
 
+bool ParseChannelArgs(v8::Local<v8::Value> args_val,
+                      grpc_channel_args **channel_args_ptr);
+
+void DeallocateChannelArgs(grpc_channel_args *channel_args);
+
 /* Wrapper class for grpc_channel structs */
 class Channel : public Nan::ObjectWrap {
  public:
diff --git a/src/node/ext/channel_credentials.cc b/src/node/ext/channel_credentials.cc
index 3d47ff2..7ca3b98 100644
--- a/src/node/ext/channel_credentials.cc
+++ b/src/node/ext/channel_credentials.cc
@@ -127,16 +127,9 @@
     info.GetReturnValue().Set(info.This());
     return;
   } else {
-    const int argc = 1;
-    Local<Value> argv[argc] = {info[0]};
-    MaybeLocal<Object> maybe_instance = constructor->GetFunction()->NewInstance(
-        argc, argv);
-    if (maybe_instance.IsEmpty()) {
-      // There's probably a pending exception
-      return;
-    } else {
-      info.GetReturnValue().Set(maybe_instance.ToLocalChecked());
-    }
+    // This should never be called directly
+    return Nan::ThrowTypeError(
+        "ChannelCredentials can only be created with the provided functions");
   }
 }
 
diff --git a/src/node/ext/server.cc b/src/node/ext/server.cc
index 87363fc..b9e1fe9 100644
--- a/src/node/ext/server.cc
+++ b/src/node/ext/server.cc
@@ -182,49 +182,14 @@
   }
   grpc_server *wrapped_server;
   grpc_completion_queue *queue = CompletionQueueAsyncWorker::GetQueue();
-  if (info[0]->IsUndefined()) {
-    wrapped_server = grpc_server_create(NULL, NULL);
-  } else if (info[0]->IsObject()) {
-    Local<Object> args_hash = Nan::To<Object>(info[0]).ToLocalChecked();
-    Local<Array> keys = Nan::GetOwnPropertyNames(args_hash).ToLocalChecked();
-    grpc_channel_args channel_args;
-    channel_args.num_args = keys->Length();
-    channel_args.args = reinterpret_cast<grpc_arg *>(
-        calloc(channel_args.num_args, sizeof(grpc_arg)));
-    /* These are used to keep all strings until then end of the block, then
-       destroy them */
-    std::vector<Utf8String *> key_strings(keys->Length());
-    std::vector<Utf8String *> value_strings(keys->Length());
-    for (unsigned int i = 0; i < channel_args.num_args; i++) {
-      MaybeLocal<String> maybe_key = Nan::To<String>(
-          Nan::Get(keys, i).ToLocalChecked());
-      if (maybe_key.IsEmpty()) {
-        free(channel_args.args);
-        return Nan::ThrowTypeError("Arg keys must be strings");
-      }
-      Local<String> current_key = maybe_key.ToLocalChecked();
-      Local<Value> current_value = Nan::Get(args_hash,
-                                             current_key).ToLocalChecked();
-      key_strings[i] = new Utf8String(current_key);
-      channel_args.args[i].key = **key_strings[i];
-      if (current_value->IsInt32()) {
-        channel_args.args[i].type = GRPC_ARG_INTEGER;
-        channel_args.args[i].value.integer = Nan::To<int32_t>(
-            current_value).FromJust();
-      } else if (current_value->IsString()) {
-        channel_args.args[i].type = GRPC_ARG_STRING;
-        value_strings[i] = new Utf8String(current_value);
-        channel_args.args[i].value.string = **value_strings[i];
-      } else {
-        free(channel_args.args);
-        return Nan::ThrowTypeError("Arg values must be strings");
-      }
-    }
-    wrapped_server = grpc_server_create(&channel_args, NULL);
-    free(channel_args.args);
-  } else {
-    return Nan::ThrowTypeError("Server expects an object");
+  grpc_channel_args *channel_args;
+  if (!ParseChannelArgs(info[0], &channel_args)) {
+    DeallocateChannelArgs(channel_args);
+    return Nan::ThrowTypeError("Server options must be an object with "
+                               "string keys and integer or string values");
   }
+  wrapped_server = grpc_server_create(channel_args, NULL);
+  DeallocateChannelArgs(channel_args);
   grpc_server_register_completion_queue(wrapped_server, queue, NULL);
   Server *server = new Server(wrapped_server);
   server->Wrap(info.This());
diff --git a/src/node/ext/server_credentials.cc b/src/node/ext/server_credentials.cc
index 5e922bd..e6c55e2 100644
--- a/src/node/ext/server_credentials.cc
+++ b/src/node/ext/server_credentials.cc
@@ -117,7 +117,7 @@
   if (info.IsConstructCall()) {
     if (!info[0]->IsExternal()) {
       return Nan::ThrowTypeError(
-          "ServerCredentials can only be created with the provide functions");
+          "ServerCredentials can only be created with the provided functions");
     }
     Local<External> ext = info[0].As<External>();
     grpc_server_credentials *creds_value =
@@ -126,16 +126,9 @@
     credentials->Wrap(info.This());
     info.GetReturnValue().Set(info.This());
   } else {
-    const int argc = 1;
-    Local<Value> argv[argc] = {info[0]};
-    MaybeLocal<Object> maybe_instance = constructor->GetFunction()->NewInstance(
-        argc, argv);
-    if (maybe_instance.IsEmpty()) {
-      // There's probably a pending exception
-      return;
-    } else {
-      info.GetReturnValue().Set(maybe_instance.ToLocalChecked());
-    }
+    // This should never be called directly
+    return Nan::ThrowTypeError(
+        "ServerCredentials can only be created with the provided functions");
   }
 }
 
diff --git a/src/node/interop/interop_client.js b/src/node/interop/interop_client.js
index cb55083..b506189 100644
--- a/src/node/interop/interop_client.js
+++ b/src/node/interop/interop_client.js
@@ -35,7 +35,6 @@
 
 var fs = require('fs');
 var path = require('path');
-var _ = require('lodash');
 var grpc = require('..');
 var testProto = grpc.load({
   root: __dirname + '/../../..',
diff --git a/src/node/src/client.js b/src/node/src/client.js
index 909376e..596ea5e 100644
--- a/src/node/src/client.js
+++ b/src/node/src/client.js
@@ -661,6 +661,7 @@
   var checkState = function(err) {
     if (err) {
       callback(new Error('Failed to connect before the deadline'));
+      return;
     }
     var new_state = client.$channel.getConnectivityState(true);
     if (new_state === grpc.connectivityState.READY) {
diff --git a/src/node/src/common.js b/src/node/src/common.js
index 5551ebe..ebaaa13 100644
--- a/src/node/src/common.js
+++ b/src/node/src/common.js
@@ -87,14 +87,9 @@
     return '';
   }
   var name = value.name;
-  if (value.className === 'Service.RPCMethod') {
-    name = _.capitalize(name);
-  }
-  if (value.hasOwnProperty('parent')) {
-    var parent_name = fullyQualifiedName(value.parent);
-    if (parent_name !== '') {
-      name = parent_name + '.' + name;
-    }
+  var parent_name = fullyQualifiedName(value.parent);
+  if (parent_name !== '') {
+    name = parent_name + '.' + name;
   }
   return name;
 };
diff --git a/src/node/src/credentials.js b/src/node/src/credentials.js
index ddc094f..ff10a22 100644
--- a/src/node/src/credentials.js
+++ b/src/node/src/credentials.js
@@ -99,6 +99,9 @@
         if (error.hasOwnProperty('code')) {
           code = error.code;
         }
+        if (!metadata) {
+          metadata = new Metadata();
+        }
       }
       callback(code, message, metadata._getCoreRepresentation());
     });
diff --git a/src/node/test/call_test.js b/src/node/test/call_test.js
index c316fe7..f1f86b3 100644
--- a/src/node/test/call_test.js
+++ b/src/node/test/call_test.js
@@ -107,6 +107,23 @@
         new grpc.Call(channel, 'method', 'now');
       }, TypeError);
     });
+    it('should succeed without the new keyword', function() {
+      assert.doesNotThrow(function() {
+        var call = grpc.Call(channel, 'method', new Date());
+        assert(call instanceof grpc.Call);
+      });
+    });
+  });
+  describe('deadline', function() {
+    it('should time out immediately with negative deadline', function(done) {
+      var call = new grpc.Call(channel, 'method', -Infinity);
+      var batch = {};
+      batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
+      call.startBatch(batch, function(err, response) {
+        assert.strictEqual(response.status.code, grpc.status.DEADLINE_EXCEEDED);
+        done();
+      });
+    });
   });
   describe('startBatch', function() {
     it('should fail without an object and a function', function() {
@@ -192,6 +209,43 @@
       });
     });
   });
+  describe('cancelWithStatus', function() {
+    it('should reject anything other than an integer and a string', function() {
+      assert.doesNotThrow(function() {
+        var call = new grpc.Call(channel, 'method', getDeadline(1));
+        call.cancelWithStatus(1, 'details');
+      });
+      assert.throws(function() {
+        var call = new grpc.Call(channel, 'method', getDeadline(1));
+        call.cancelWithStatus();
+      });
+      assert.throws(function() {
+        var call = new grpc.Call(channel, 'method', getDeadline(1));
+        call.cancelWithStatus('');
+      });
+      assert.throws(function() {
+        var call = new grpc.Call(channel, 'method', getDeadline(1));
+        call.cancelWithStatus(5, {});
+      });
+    });
+    it('should reject the OK status code', function() {
+      assert.throws(function() {
+        var call = new grpc.Call(channel, 'method', getDeadline(1));
+        call.cancelWithStatus(0, 'details');
+      });
+    });
+    it('should result in the call ending with a status', function(done) {
+      var call = new grpc.Call(channel, 'method', getDeadline(1));
+      var batch = {};
+      batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
+      call.startBatch(batch, function(err, response) {
+        assert.strictEqual(response.status.code, 5);
+        assert.strictEqual(response.status.details, 'details');
+        done();
+      });
+      call.cancelWithStatus(5, 'details');
+    });
+  });
   describe('getPeer', function() {
     it('should return a string', function() {
       var call = new grpc.Call(channel, 'method', getDeadline(1));
diff --git a/src/node/test/channel_test.js b/src/node/test/channel_test.js
index 05269f7..7163a5f 100644
--- a/src/node/test/channel_test.js
+++ b/src/node/test/channel_test.js
@@ -104,6 +104,12 @@
         new grpc.Channel('hostname', insecureCreds, {'key' : new Date()});
       });
     });
+    it('should succeed without the new keyword', function() {
+      assert.doesNotThrow(function() {
+        var channel = grpc.Channel('hostname', insecureCreds);
+        assert(channel instanceof grpc.Channel);
+      });
+    });
   });
   describe('close', function() {
     var channel;
@@ -155,7 +161,6 @@
       deadline.setSeconds(deadline.getSeconds() + 1);
       channel.watchConnectivityState(old_state, deadline, function(err, value) {
         assert(err);
-        console.log('Callback from watchConnectivityState');
         done();
       });
     });
diff --git a/src/node/test/credentials_test.js b/src/node/test/credentials_test.js
index 7fc311a..3d0b38f 100644
--- a/src/node/test/credentials_test.js
+++ b/src/node/test/credentials_test.js
@@ -60,6 +60,22 @@
   };
 }
 
+var fakeSuccessfulGoogleCredentials = {
+  getRequestMetadata: function(service_url, callback) {
+    setTimeout(function() {
+      callback(null, {Authorization: 'success'});
+    }, 0);
+  }
+};
+
+var fakeFailingGoogleCredentials = {
+  getRequestMetadata: function(service_url, callback) {
+    setTimeout(function() {
+      callback(new Error("Authorization failure"));
+    }, 0);
+  }
+};
+
 describe('client credentials', function() {
   var Client;
   var server;
@@ -169,6 +185,52 @@
       done();
     });
   });
+  it.skip('should propagate errors that the updater emits', function(done) {
+    var metadataUpdater = function(service_url, callback) {
+      var error = new Error('Authentication error');
+      error.code = grpc.status.UNAUTHENTICATED;
+      callback(error);
+    };
+    var creds = grpc.credentials.createFromMetadataGenerator(metadataUpdater);
+    var combined_creds = grpc.credentials.combineChannelCredentials(
+        client_ssl_creds, creds);
+    var client = new Client('localhost:' + port, combined_creds,
+                            client_options);
+    client.unary({}, function(err, data) {
+      assert(err);
+      assert.strictEqual(err.message, 'Authentication error');
+      assert.strictEqual(err.code, grpc.status.UNAUTHENTICATED);
+      done();
+    });
+  });
+  it('should successfully wrap a Google credential', function(done) {
+    var creds = grpc.credentials.createFromGoogleCredential(
+        fakeSuccessfulGoogleCredentials);
+    var combined_creds = grpc.credentials.combineChannelCredentials(
+        client_ssl_creds, creds);
+    var client = new Client('localhost:' + port, combined_creds,
+                            client_options);
+    var call = client.unary({}, function(err, data) {
+      assert.ifError(err);
+    });
+    call.on('metadata', function(metadata) {
+      assert.deepEqual(metadata.get('authorization'), ['success']);
+      done();
+    });
+  });
+  it.skip('should get an error from a Google credential', function(done) {
+    var creds = grpc.credentials.createFromGoogleCredential(
+        fakeFailingGoogleCredentials);
+    var combined_creds = grpc.credentials.combineChannelCredentials(
+        client_ssl_creds, creds);
+    var client = new Client('localhost:' + port, combined_creds,
+                            client_options);
+    client.unary({}, function(err, data) {
+      assert(err);
+      assert.strictEqual(err.message, 'Authorization failure');
+      done();
+    });
+  });
   describe('Per-rpc creds', function() {
     var client;
     var updater_creds;
diff --git a/src/node/test/health_test.js b/src/node/test/health_test.js
index a4dc24c..c93b528 100644
--- a/src/node/test/health_test.js
+++ b/src/node/test/health_test.js
@@ -45,11 +45,13 @@
     'grpc.test.TestServiceNotServing': 'NOT_SERVING',
     'grpc.test.TestServiceServing': 'SERVING'
   };
-  var healthServer = new grpc.Server();
-  healthServer.addProtoService(health.service,
-                               new health.Implementation(statusMap));
+  var healthServer;
+  var healthImpl;
   var healthClient;
   before(function() {
+    healthServer = new grpc.Server();
+    healthImpl = new health.Implementation(statusMap);
+    healthServer.addProtoService(health.service, healthImpl);
     var port_num = healthServer.bind('0.0.0.0:0',
                                      grpc.ServerCredentials.createInsecure());
     healthServer.start();
@@ -89,4 +91,16 @@
       done();
     });
   });
+  it('should get a different response if the status changes', function(done) {
+    healthClient.check({service: 'transient'}, function(err, response) {
+      assert(err);
+      assert.strictEqual(err.code, grpc.status.NOT_FOUND);
+      healthImpl.setStatus('transient', 'SERVING');
+      healthClient.check({service: 'transient'}, function(err, response) {
+        assert.ifError(err);
+        assert.strictEqual(response.status, 'SERVING');
+        done();
+      });
+    });
+  });
 });
diff --git a/src/node/test/interop_sanity_test.js b/src/node/test/interop_sanity_test.js
index f8c0b14..f008a87 100644
--- a/src/node/test/interop_sanity_test.js
+++ b/src/node/test/interop_sanity_test.js
@@ -71,7 +71,7 @@
     interop_client.runTest(port, name_override, 'server_streaming', true, true,
                            done);
   });
-  it.only('should pass ping_pong', function(done) {
+  it('should pass ping_pong', function(done) {
     interop_client.runTest(port, name_override, 'ping_pong', true, true, done);
   });
   it('should pass empty_stream', function(done) {
diff --git a/src/node/test/surface_test.js b/src/node/test/surface_test.js
index 395ea88..39673e4 100644
--- a/src/node/test/surface_test.js
+++ b/src/node/test/surface_test.js
@@ -92,6 +92,31 @@
     });
   });
 });
+describe('surface Server', function() {
+  var server;
+  beforeEach(function() {
+    server = new grpc.Server();
+  });
+  afterEach(function() {
+    server.forceShutdown();
+  });
+  it('should error if started twice', function() {
+    server.start();
+    assert.throws(function() {
+      server.start();
+    });
+  });
+  it('should error if a port is bound after the server starts', function() {
+    server.start();
+    assert.throws(function() {
+      server.bind('localhost:0', grpc.ServerCredentials.createInsecure());
+    });
+  });
+  it('should successfully shutdown if tryShutdown is called', function(done) {
+    server.start();
+    server.tryShutdown(done);
+  });
+});
 describe('Server.prototype.addProtoService', function() {
   var server;
   var dummyImpls = {
@@ -202,6 +227,16 @@
       });
     });
   });
+  it('should time out if the server does not exist', function(done) {
+    var bad_client = new Client('nonexistent_hostname',
+                                grpc.credentials.createInsecure());
+    var deadline = new Date();
+    deadline.setSeconds(deadline.getSeconds() + 1);
+    grpc.waitForClientReady(bad_client, deadline, function(error) {
+      assert(error);
+      done();
+    });
+  });
 });
 describe('Echo service', function() {
   var server;
@@ -365,6 +400,123 @@
       done();
     });
   });
+  it('properly handles duplicate values', function(done) {
+    var dup_metadata = metadata.clone();
+    dup_metadata.add('key', 'value2');
+    var call = client.unary({}, function(err, data) {assert.ifError(err); },
+                            dup_metadata);
+    call.on('metadata', function(resp_metadata) {
+      // Two arrays are equal iff their symmetric difference is empty
+      assert.deepEqual(_.xor(dup_metadata.get('key'), resp_metadata.get('key')),
+                       []);
+      done();
+    });
+  });
+});
+describe('Client malformed response handling', function() {
+  var server;
+  var client;
+  var badArg = new Buffer([0xFF]);
+  before(function() {
+    var test_proto = ProtoBuf.loadProtoFile(__dirname + '/test_service.proto');
+    var test_service = test_proto.lookup('TestService');
+    var malformed_test_service = {
+      unary: {
+        path: '/TestService/Unary',
+        requestStream: false,
+        responseStream: false,
+        requestDeserialize: _.identity,
+        responseSerialize: _.identity
+      },
+      clientStream: {
+        path: '/TestService/ClientStream',
+        requestStream: true,
+        responseStream: false,
+        requestDeserialize: _.identity,
+        responseSerialize: _.identity
+      },
+      serverStream: {
+        path: '/TestService/ServerStream',
+        requestStream: false,
+        responseStream: true,
+        requestDeserialize: _.identity,
+        responseSerialize: _.identity
+      },
+      bidiStream: {
+        path: '/TestService/BidiStream',
+        requestStream: true,
+        responseStream: true,
+        requestDeserialize: _.identity,
+        responseSerialize: _.identity
+      }
+    };
+    server = new grpc.Server();
+    server.addService(malformed_test_service, {
+      unary: function(call, cb) {
+        cb(null, badArg);
+      },
+      clientStream: function(stream, cb) {
+        stream.on('data', function() {/* Ignore requests */});
+        stream.on('end', function() {
+          cb(null, badArg);
+        });
+      },
+      serverStream: function(stream) {
+        stream.write(badArg);
+        stream.end();
+      },
+      bidiStream: function(stream) {
+        stream.on('data', function() {
+          // Ignore requests
+          stream.write(badArg);
+        });
+        stream.on('end', function() {
+          stream.end();
+        });
+      }
+    });
+    var port = server.bind('localhost:0', server_insecure_creds);
+    var Client = surface_client.makeProtobufClientConstructor(test_service);
+    client = new Client('localhost:' + port, grpc.credentials.createInsecure());
+    server.start();
+  });
+  after(function() {
+    server.forceShutdown();
+  });
+  it('should get an INTERNAL status with a unary call', function(done) {
+    client.unary({}, function(err, data) {
+      assert(err);
+      assert.strictEqual(err.code, grpc.status.INTERNAL);
+      done();
+    });
+  });
+  it('should get an INTERNAL status with a client stream call', function(done) {
+    var call = client.clientStream(function(err, data) {
+      assert(err);
+      assert.strictEqual(err.code, grpc.status.INTERNAL);
+      done();
+    });
+    call.write({});
+    call.end();
+  });
+  it('should get an INTERNAL status with a server stream call', function(done) {
+    var call = client.serverStream({});
+    call.on('data', function(){});
+    call.on('error', function(err) {
+      assert.strictEqual(err.code, grpc.status.INTERNAL);
+      done();
+    });
+  });
+  it('should get an INTERNAL status with a bidi stream call', function(done) {
+    var call = client.bidiStream();
+    call.on('data', function(){});
+    call.on('error', function(err) {
+      assert.strictEqual(err.code, grpc.status.INTERNAL);
+      done();
+    });
+    call.write({});
+    call.end();
+  });
 });
 describe('Other conditions', function() {
   var test_service;
@@ -382,7 +534,8 @@
       unary: function(call, cb) {
         var req = call.request;
         if (req.error) {
-          cb(new Error('Requested error'), null, trailer_metadata);
+          cb({code: grpc.status.UNKNOWN,
+              details: 'Requested error'}, null, trailer_metadata);
         } else {
           cb(null, {count: 1}, trailer_metadata);
         }
@@ -407,7 +560,8 @@
       serverStream: function(stream) {
         var req = stream.request;
         if (req.error) {
-          var err = new Error('Requested error');
+          var err = {code: grpc.status.UNKNOWN,
+                     details: 'Requested error'};
           err.metadata = trailer_metadata;
           stream.emit('error', err);
         } else {
@@ -447,6 +601,23 @@
     assert.strictEqual(typeof grpc.getClientChannel(client).getTarget(),
                        'string');
   });
+  it('client should be able to pause and resume a stream', function(done) {
+    var call = client.bidiStream();
+    call.on('data', function(data) {
+      assert(data.count < 3);
+      call.pause();
+      setTimeout(function() {
+        call.resume();
+      }, 10);
+    });
+    call.on('end', function() {
+      done();
+    });
+    call.write({});
+    call.write({});
+    call.write({});
+    call.end();
+  });
   describe('Server recieving bad input', function() {
     var misbehavingClient;
     var badArg = new Buffer([0xFF]);
diff --git a/templates/binding.gyp.template b/templates/binding.gyp.template
index 50d0823..bfa6a56 100644
--- a/templates/binding.gyp.template
+++ b/templates/binding.gyp.template
@@ -56,6 +56,24 @@
       'include_dirs': [
         '.',
         'include'
+      ],
+      'conditions': [
+        ['OS != "win"', {
+          'conditions': [
+            ['config=="gcov"', {
+              'cflags': [
+                '-ftest-coverage',
+                '-fprofile-arcs',
+                '-O0'
+              ],
+              'ldflags': [
+                '-ftest-coverage',
+                '-fprofile-arcs'
+              ]
+            }
+           ]
+          ]
+        }],
       ]
     },
     'targets': [
@@ -95,22 +113,6 @@
           '-g'
         ],
         "conditions": [
-          ['OS != "win"', {
-            'conditions': [
-              ['config=="gcov"', {
-                'cflags': [
-                  '-ftest-coverage',
-                  '-fprofile-arcs',
-                  '-O0'
-                ],
-                'ldflags': [
-                  '-ftest-coverage',
-                  '-fprofile-arcs'
-                ]
-              }
-             ]
-            ]
-          }],
           ['OS == "mac"', {
             'xcode_settings': {
               'MACOSX_DEPLOYMENT_TARGET': '10.9',
diff --git a/tools/run_tests/run_node.sh b/tools/run_tests/run_node.sh
index 0a11e87..85e3a38 100755
--- a/tools/run_tests/run_node.sh
+++ b/tools/run_tests/run_node.sh
@@ -45,7 +45,8 @@
   gcov Release/obj.target/grpc/ext/*.o
   lcov --base-directory . --directory . -c -o coverage.info
   genhtml -o ../reports/node_ext_coverage --num-spaces 2 \
-    -t 'Node gRPC test coverage' coverage.info
+    -t 'Node gRPC test coverage' coverage.info --rc genhtml_hi_limit=95 \
+    --rc genhtml_med_limit=80
   echo '<html><head><meta http-equiv="refresh" content="0;URL=lcov-report/index.html"></head></html>' > \
     ../reports/node_coverage/index.html
 else