diff --git a/appengine/analytics/package.json b/appengine/analytics/package.json index 835b90ffc9..4eda3694a6 100644 --- a/appengine/analytics/package.json +++ b/appengine/analytics/package.json @@ -10,13 +10,10 @@ }, "scripts": { "start": "node app.js", - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- appengine/analytics/test/*.test.js" }, "dependencies": { - "express": "^4.14.0", - "request": "^2.75.0" - }, - "devDependencies": { - "mocha": "^3.1.0" + "express": "4.14.0", + "request": "2.78.0" } } diff --git a/appengine/bower/package.json b/appengine/bower/package.json index 86283b04aa..22385970fe 100644 --- a/appengine/bower/package.json +++ b/appengine/bower/package.json @@ -10,14 +10,11 @@ }, "scripts": { "postinstall": "bower install --config.interactive=false", - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- appengine/analytics/test/*.test.js" }, "dependencies": { - "bower": "^1.7.9", - "express": "^4.14.0", - "pug": "^2.0.0-beta6" - }, - "devDependencies": { - "mocha": "^3.1.0" + "bower": "1.8.0", + "express": "4.14.0", + "pug": "2.0.0-beta6" } } diff --git a/appengine/cloudsql/package.json b/appengine/cloudsql/package.json index 07d64ed0b7..d3f430bece 100644 --- a/appengine/cloudsql/package.json +++ b/appengine/cloudsql/package.json @@ -9,14 +9,11 @@ "node": ">=4.3.2" }, "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- appengine/analytics/test/*.test.js" }, "dependencies": { - "express": "^4.14.0", - "mysql": "^2.11.1", - "prompt": "^1.0.0" - }, - "devDependencies": { - "mocha": "^3.1.0" + "express": "4.14.0", + "mysql": "2.12.0", + "prompt": "1.0.0" } } diff --git a/appengine/datastore/app.js b/appengine/datastore/app.js index fe8d09298b..7f779d178b 100644 --- a/appengine/datastore/app.js +++ b/appengine/datastore/app.js @@ -71,7 +71,7 @@ function getVisits (callback) { callback(err); return; } - callback(null, entities.map((entity) => `Time: ${entity.data.timestamp}, AddrHash: ${entity.data.userIp}`)); + callback(null, entities.map((entity) => `Time: ${entity.timestamp}, AddrHash: ${entity.userIp}`)); }); } // [END getVisits] diff --git a/appengine/datastore/package.json b/appengine/datastore/package.json index da5c9f01a8..e48892708b 100644 --- a/appengine/datastore/package.json +++ b/appengine/datastore/package.json @@ -10,13 +10,10 @@ }, "scripts": { "start": "node app.js", - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- appengine/analytics/test/*.test.js" }, "dependencies": { - "@google-cloud/datastore": "^0.4.0", - "express": "^4.14.0" - }, - "devDependencies": { - "mocha": "^3.1.0" + "@google-cloud/datastore": "0.5.0", + "express": "4.14.0" } } diff --git a/appengine/datastore/test/app.test.js b/appengine/datastore/test/app.test.js index 4d045c5549..b89f11a26b 100644 --- a/appengine/datastore/test/app.test.js +++ b/appengine/datastore/test/app.test.js @@ -39,10 +39,8 @@ function getSample () { const expressMock = sinon.stub().returns(testApp); const resultsMock = [ { - data: { - timestamp: `1234`, - userIp: `abcd` - } + timestamp: `1234`, + userIp: `abcd` } ]; const queryMock = { diff --git a/appengine/disk/package.json b/appengine/disk/package.json index 780641bdd4..0bdb044696 100644 --- a/appengine/disk/package.json +++ b/appengine/disk/package.json @@ -10,12 +10,9 @@ }, "scripts": { "start": "node app.js", - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- appengine/analytics/test/*.test.js" }, "dependencies": { - "express": "^4.14.0" - }, - "devDependencies": { - "mocha": "^3.1.0" + "express": "4.14.0" } } diff --git a/appengine/endpoints/package.json b/appengine/endpoints/package.json index 8627da51b2..b36946318f 100644 --- a/appengine/endpoints/package.json +++ b/appengine/endpoints/package.json @@ -6,17 +6,14 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "engines": { - "node": "~4.2" + "node": ">=4.3.2" }, "scripts": { "start": "node app.js", - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- appengine/analytics/test/*.test.js" }, "dependencies": { - "express": "^4.13.4", - "body-parser": "^1.15.0" - }, - "devDependencies": { - "mocha": "^2.5.3" + "express": "4.14.0", + "body-parser": "1.15.2" } } diff --git a/appengine/errorreporting/package.json b/appengine/errorreporting/package.json index cf10d55057..f16afef5b4 100644 --- a/appengine/errorreporting/package.json +++ b/appengine/errorreporting/package.json @@ -10,13 +10,10 @@ }, "scripts": { "start": "node app.js", - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- appengine/analytics/test/*.test.js" }, "dependencies": { - "express": "^4.14.0", - "winston": "^2.2.0" - }, - "devDependencies": { - "mocha": "^3.1.0" + "express": "4.14.0", + "winston": "2.3.0" } } diff --git a/appengine/express-memcached-session/package.json b/appengine/express-memcached-session/package.json index d71bf20d8e..70a85cb5e6 100644 --- a/appengine/express-memcached-session/package.json +++ b/appengine/express-memcached-session/package.json @@ -10,16 +10,13 @@ }, "scripts": { "start": "node server.js", - "test": "mocha -R spec -t 1000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- appengine/analytics/test/*.test.js" }, "dependencies": { - "connect-memjs": "^0.1.0", - "cookie-parser": "^1.4.3", - "express": "^4.14.0", - "express-session": "^1.14.2", - "public-ip": "^2.0.1" - }, - "devDependencies": { - "mocha": "^3.1.2" + "connect-memjs": "0.1.0", + "cookie-parser": "1.4.3", + "express": "4.14.0", + "express-session": "1.14.2", + "public-ip": "2.0.1" } } diff --git a/appengine/express/package.json b/appengine/express/package.json index 3d15a96164..0f104d01c7 100644 --- a/appengine/express/package.json +++ b/appengine/express/package.json @@ -10,18 +10,15 @@ }, "scripts": { "start": "node ./bin/www", - "test": "mocha -R spec -t 1000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- appengine/analytics/test/*.test.js" }, "dependencies": { - "body-parser": "^1.15.2", - "cookie-parser": "^1.4.3", - "debug": "^2.3.2", - "express": "^4.14.0", - "morgan": "^1.7.0", - "pug": "^2.0.0-beta6", - "serve-favicon": "^2.3.0" - }, - "devDependencies": { - "mocha": "^3.1.2" + "body-parser": "1.15.2", + "cookie-parser": "1.4.3", + "debug": "2.3.2", + "express": "4.14.0", + "morgan": "1.7.0", + "pug": "2.0.0-beta6", + "serve-favicon": "2.3.0" } } diff --git a/appengine/extending-runtime/package.json b/appengine/extending-runtime/package.json index 12a064119d..21a4bb1858 100644 --- a/appengine/extending-runtime/package.json +++ b/appengine/extending-runtime/package.json @@ -12,6 +12,6 @@ "start": "node app.js" }, "dependencies": { - "express": "^4.14.0" + "express": "4.14.0" } } diff --git a/appengine/geddy/package.json b/appengine/geddy/package.json index 5b436e0976..b63640f9ce 100644 --- a/appengine/geddy/package.json +++ b/appengine/geddy/package.json @@ -14,6 +14,6 @@ "debug": "geddy --debug" }, "dependencies": { - "geddy": "^13.0.8" + "geddy": "13.0.8" } } diff --git a/appengine/grunt/package.json b/appengine/grunt/package.json index 13a724b6f5..40a7cb0673 100644 --- a/appengine/grunt/package.json +++ b/appengine/grunt/package.json @@ -13,18 +13,18 @@ "postinstall": "grunt build" }, "dependencies": { - "body-parser": "^1.15.2", - "cookie-parser": "^1.4.3", - "debug": "^2.2.0", - "express": "^4.14.0", - "grunt": "^1.0.1", - "grunt-cli": "^1.2.0", - "grunt-contrib-clean": "^1.0.0", - "grunt-contrib-cssmin": "^1.0.2", - "grunt-contrib-jshint": "^1.0.0", - "grunt-contrib-watch": "^1.0.0", - "morgan": "^1.7.0", - "pug": "^2.0.0-beta6", - "serve-favicon": "^2.3.0" + "body-parser": "1.15.2", + "cookie-parser": "1.4.3", + "debug": "2.3.2", + "express": "4.14.0", + "grunt": "1.0.1", + "grunt-cli": "1.2.0", + "grunt-contrib-clean": "1.0.0", + "grunt-contrib-cssmin": "1.0.2", + "grunt-contrib-jshint": "1.0.0", + "grunt-contrib-watch": "1.0.0", + "morgan": "1.7.0", + "pug": "2.0.0-beta6", + "serve-favicon": "2.3.0" } } diff --git a/appengine/hapi/package.json b/appengine/hapi/package.json index bd342ca3c1..195458b6ef 100644 --- a/appengine/hapi/package.json +++ b/appengine/hapi/package.json @@ -12,6 +12,6 @@ "start": "node index.js" }, "dependencies": { - "hapi": "^15.1.1" + "hapi": "15.2.0" } } diff --git a/appengine/hello-world/package.json b/appengine/hello-world/package.json index abbba4903b..e53c0ead97 100644 --- a/appengine/hello-world/package.json +++ b/appengine/hello-world/package.json @@ -12,6 +12,6 @@ "start": "node app.js" }, "dependencies": { - "express": "^4.14.0" + "express": "4.14.0" } } diff --git a/appengine/koa/package.json b/appengine/koa/package.json index 6a6354d388..056ce0ba76 100644 --- a/appengine/koa/package.json +++ b/appengine/koa/package.json @@ -12,6 +12,6 @@ "start": "node --harmony app.js" }, "dependencies": { - "koa": "^1.2.4" + "koa": "1.2.4" } } diff --git a/appengine/logging/package.json b/appengine/logging/package.json index 4a131c0920..db61a535bb 100644 --- a/appengine/logging/package.json +++ b/appengine/logging/package.json @@ -12,8 +12,8 @@ "start": "node app.js" }, "dependencies": { - "express": "^4.14.0", - "winston": "^2.2.0", - "winston-gae": "^0.1.0" + "express": "4.14.0", + "winston": "2.3.0", + "winston-gae": "0.1.0" } } diff --git a/appengine/loopback/package.json b/appengine/loopback/package.json index 182f0e413c..c1bbc98c77 100644 --- a/appengine/loopback/package.json +++ b/appengine/loopback/package.json @@ -10,18 +10,17 @@ }, "scripts": { "pretest": "jshint .", - "start": "node server/server.js", - "deploy": "gcloud app deploy" + "start": "node server/server.js" }, "dependencies": { - "compression": "^1.0.3", - "cors": "^2.5.2", - "errorhandler": "^1.1.1", - "jshint": "^2.5.6", - "loopback": "^2.14.0", - "loopback-boot": "^2.6.5", - "loopback-datasource-juggler": "^2.19.0", - "loopback-explorer": "^1.1.0", - "serve-favicon": "^2.0.1" + "compression": "1.0.3", + "cors": "2.5.2", + "errorhandler": "1.1.1", + "jshint": "2.5.6", + "loopback": "2.14.0", + "loopback-boot": "2.6.5", + "loopback-datasource-juggler": "2.19.0", + "loopback-explorer": "1.1.0", + "serve-favicon": "2.0.1" } } diff --git a/appengine/mailgun/package.json b/appengine/mailgun/package.json index 130a24500c..db1fae8eda 100644 --- a/appengine/mailgun/package.json +++ b/appengine/mailgun/package.json @@ -6,16 +6,15 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "engines": { - "node": "~4.2" + "node": ">=4.3.2" }, "scripts": { - "start": "node app.js", - "deploy": "gcloud app deploy" + "start": "node app.js" }, "dependencies": { - "body-parser": "^1.15.2", - "express": "^4.14.0", - "mailgun": "^0.5.0", - "pug": "^2.0.0-beta6" + "body-parser": "1.15.2", + "express": "4.14.0", + "mailgun": "0.5.0", + "pug": "2.0.0-beta6" } } diff --git a/appengine/mailjet/package.json b/appengine/mailjet/package.json index e35a8f12ac..c8fb63be49 100644 --- a/appengine/mailjet/package.json +++ b/appengine/mailjet/package.json @@ -6,16 +6,15 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "engines": { - "node": "~4.2" + "node": ">=4.3.2" }, "scripts": { - "start": "node app.js", - "deploy": "gcloud app deploy" + "start": "node app.js" }, "dependencies": { - "body-parser": "^1.14.2", - "express": "^4.13.4", - "jade": "^1.11.0", - "node-mailjet": "^1.1.0" + "body-parser": "1.15.2", + "express": "4.14.0", + "jade": "1.11.0", + "node-mailjet": "1.1.0" } } diff --git a/appengine/memcached/package.json b/appengine/memcached/package.json index ed5123b167..57aa58208e 100644 --- a/appengine/memcached/package.json +++ b/appengine/memcached/package.json @@ -12,7 +12,7 @@ "start": "node app.js" }, "dependencies": { - "express": "^4.14.0", - "memjs": "^0.10.0" + "express": "4.14.0", + "memjs": "0.10.0" } } diff --git a/appengine/mongodb/package.json b/appengine/mongodb/package.json index 90df9e5dea..ce85547320 100644 --- a/appengine/mongodb/package.json +++ b/appengine/mongodb/package.json @@ -9,7 +9,7 @@ "node": ">=4.3.2" }, "dependencies": { - "nconf": "^0.8.4", - "mongodb": "^2.2.10" + "nconf": "0.8.4", + "mongodb": "2.2.11" } } diff --git a/appengine/parse-server/package.json b/appengine/parse-server/package.json index cf3fda77f4..70ccc5f1fb 100644 --- a/appengine/parse-server/package.json +++ b/appengine/parse-server/package.json @@ -9,8 +9,8 @@ "node": ">=4.3.2" }, "dependencies": { - "express": "^4.14.0", - "parse-server": "^2.2.22", - "nconf": "^0.8.4" + "express": "4.14.0", + "parse-server": "2.2.22", + "nconf": "0.8.4" } } diff --git a/appengine/pubsub/package.json b/appengine/pubsub/package.json index 5e55cd7e2e..114ec21dbd 100644 --- a/appengine/pubsub/package.json +++ b/appengine/pubsub/package.json @@ -12,9 +12,9 @@ "start": "node app.js" }, "dependencies": { - "@google-cloud/pubsub": "^0.3.0", - "body-parser": "^1.15.2", - "express": "^4.14.0", - "pug": "^2.0.0-beta6" + "@google-cloud/pubsub": "0.3.0", + "body-parser": "1.15.2", + "express": "4.14.0", + "pug": "2.0.0-beta6" } } diff --git a/appengine/redis/package.json b/appengine/redis/package.json index f9efcf1c0a..89fc875d54 100644 --- a/appengine/redis/package.json +++ b/appengine/redis/package.json @@ -9,7 +9,7 @@ "node": ">=4.3.2" }, "dependencies": { - "nconf": "^0.8.4", - "redis": "^2.6.2" + "nconf": "0.8.4", + "redis": "2.6.2" } } diff --git a/appengine/restify/package.json b/appengine/restify/package.json index d30d677871..18e84f4e9d 100644 --- a/appengine/restify/package.json +++ b/appengine/restify/package.json @@ -9,6 +9,6 @@ "node": ">=4.3.2" }, "dependencies": { - "restify": "^4.0.0" + "restify": "4.0.0" } } diff --git a/appengine/sails/package.json b/appengine/sails/package.json index a70794bfa0..d3a185cea2 100644 --- a/appengine/sails/package.json +++ b/appengine/sails/package.json @@ -13,22 +13,22 @@ "start": "node app.js" }, "dependencies": { - "ejs": "^2.3.4", + "ejs": "2.3.4", "grunt": "0.4.5", - "grunt-contrib-clean": "^0.6.0", - "grunt-contrib-coffee": "^0.13.0", - "grunt-contrib-concat": "^0.5.1", - "grunt-contrib-copy": "^0.8.1", - "grunt-contrib-cssmin": "^0.14.0", - "grunt-contrib-jst": "^0.6.0", - "grunt-contrib-less": "^1.0.1", - "grunt-contrib-uglify": "^0.9.2", - "grunt-contrib-watch": "^0.6.1", - "grunt-sails-linker": "^0.10.1", - "grunt-sync": "^0.4.1", - "include-all": "^0.1.6", - "rc": "^1.1.1", - "sails": "^0.11.2", - "sails-disk": "^0.10.0" + "grunt-contrib-clean": "0.6.0", + "grunt-contrib-coffee": "0.13.0", + "grunt-contrib-concat": "0.5.1", + "grunt-contrib-copy": "0.8.1", + "grunt-contrib-cssmin": "0.14.0", + "grunt-contrib-jst": "0.6.0", + "grunt-contrib-less": "1.0.1", + "grunt-contrib-uglify": "0.9.2", + "grunt-contrib-watch": "0.6.1", + "grunt-sails-linker": "0.10.1", + "grunt-sync": "0.4.1", + "include-all": "0.1.6", + "rc": "1.1.1", + "sails": "0.11.2", + "sails-disk": "0.10.0" } } \ No newline at end of file diff --git a/appengine/sendgrid/package.json b/appengine/sendgrid/package.json index c5530d6151..487cb77aa0 100644 --- a/appengine/sendgrid/package.json +++ b/appengine/sendgrid/package.json @@ -12,9 +12,9 @@ "start": "node app.js" }, "dependencies": { - "body-parser": "^1.14.2", - "express": "^4.14.0", - "pug": "^2.0.0-beta6", - "sendgrid": "^4.0.1" + "body-parser": "1.14.2", + "express": "4.14.0", + "pug": "2.0.0-beta6", + "sendgrid": "4.0.1" } } diff --git a/appengine/static-files/package.json b/appengine/static-files/package.json index c075e80798..d1e0e6b7bf 100644 --- a/appengine/static-files/package.json +++ b/appengine/static-files/package.json @@ -12,7 +12,7 @@ "start": "node app.js" }, "dependencies": { - "express": "^4.14.0", - "pug": "^2.0.0-beta6" + "express": "4.14.0", + "pug": "2.0.0-beta6" } } diff --git a/appengine/storage/package.json b/appengine/storage/package.json index 627c256d34..5a3c9bcf24 100644 --- a/appengine/storage/package.json +++ b/appengine/storage/package.json @@ -8,10 +8,10 @@ "node": ">=4.3.2" }, "dependencies": { - "@google-cloud/storage": "^0.3.0", - "body-parser": "^1.15.2", - "express": "^4.14.0", - "multer": "^1.2.0", - "pug": "^2.0.0-beta6" + "@google-cloud/storage": "0.3.0", + "body-parser": "1.15.2", + "express": "4.14.0", + "multer": "1.2.0", + "pug": "2.0.0-beta6" } } diff --git a/appengine/twilio/package.json b/appengine/twilio/package.json index 8d2db34852..00c9a3911b 100644 --- a/appengine/twilio/package.json +++ b/appengine/twilio/package.json @@ -12,8 +12,8 @@ "start": "node app.js" }, "dependencies": { - "body-parser": "^1.15.2", - "express": "^4.14.0", - "twilio": "^2.9.0" + "body-parser": "1.15.2", + "express": "4.14.0", + "twilio": "2.9.0" } } diff --git a/appengine/webpack/package.json b/appengine/webpack/package.json index d5737d9e2e..41d93cb4ec 100644 --- a/appengine/webpack/package.json +++ b/appengine/webpack/package.json @@ -13,8 +13,8 @@ "prestart": "npm run bundle" }, "dependencies": { - "express": "^4.14.0", - "pug": "^2.0.0-beta6", - "webpack": "^1.12.13" + "express": "4.14.0", + "pug": "2.0.0-beta6", + "webpack": "1.12.13" } } diff --git a/appengine/websockets/package.json b/appengine/websockets/package.json index b62e6ef361..24df72f0e5 100644 --- a/appengine/websockets/package.json +++ b/appengine/websockets/package.json @@ -12,9 +12,9 @@ "start": "node app.js" }, "dependencies": { - "express": "^4.14.0", - "express-ws": "^1.0.0-rc.2", - "pug": "^2.0.0-beta6", - "request": "^2.75.0" + "express": "4.14.0", + "express-ws": "1.0.0-rc.2", + "pug": "2.0.0-beta6", + "request": "2.78.0" } } diff --git a/bigquery/package.json b/bigquery/package.json index fcc48d77a6..2ecfa97c1e 100644 --- a/bigquery/package.json +++ b/bigquery/package.json @@ -5,17 +5,16 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "scripts": { - "test": "mocha -R spec -t 10000 --require intelli-espower-loader ../test/_setup.js test/*.test.js", - "system-test": "mocha -R spec -t 10000 --require intelli-espower-loader ../system-test/_setup.js system-test/*.test.js" + "test": "cd ..; npm run t -- bigquery/test/*.test.js", + "system-test": "cd ..; npm run st -- bigquery/system-test/*.test.js" }, "dependencies": { - "@google-cloud/bigquery": "^0.4.0", - "@google-cloud/storage": "^0.4.0", - "async": "^2.1.2", - "yargs": "^6.3.0" + "@google-cloud/bigquery": "0.4.0", + "@google-cloud/storage": "0.4.0", + "async": "2.1.2", + "yargs": "6.3.0" }, "devDependencies": { - "mocha": "^3.1.2", "node-uuid": "^1.4.7" }, "engines": { diff --git a/debugger/package.json b/debugger/package.json index dabf71d10d..32246bed4d 100644 --- a/debugger/package.json +++ b/debugger/package.json @@ -5,7 +5,7 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "engines": { - "node": "~4.2" + "node": ">=4.3.2" }, "scripts": { "start": "node app.js", diff --git a/dns/package.json b/dns/package.json index 4a8aceedc3..79150fcd78 100644 --- a/dns/package.json +++ b/dns/package.json @@ -5,16 +5,15 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../test/_setup.js test/*.test.js", - "system-test": "mocha -R spec -t 120000 --require intelli-espower-loader ../system-test/_setup.js system-test/*.test.js" + "test": "cd ..; npm run t -- dns/test/*.test.js", + "system-test": "cd ..; npm run st -- dns/system-test/*.test.js" }, "dependencies": { - "@google-cloud/dns": "^0.2.0", - "yargs": "^6.0.0" + "@google-cloud/dns": "0.2.0", + "yargs": "6.4.0" }, "devDependencies": { - "mocha": "^3.1.0", - "uuid": "^2.0.3" + "uuid": "2.0.3" }, "engines": { "node": ">=4.3.2" diff --git a/endpoints/getting-started/package.json b/endpoints/getting-started/package.json index 8627da51b2..90b02a156f 100644 --- a/endpoints/getting-started/package.json +++ b/endpoints/getting-started/package.json @@ -6,7 +6,7 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "engines": { - "node": "~4.2" + "node": ">=4.3.2" }, "scripts": { "start": "node app.js", diff --git a/functions/background/index.js b/functions/background/index.js index 9f65e6f788..2237056ba9 100644 --- a/functions/background/index.js +++ b/functions/background/index.js @@ -1,68 +1,72 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -// [START helloworld] +// [START functions_background_helloworld] /** * Background Cloud Function. * - * @param {Object} context Cloud Function context. - * @param {Object} data Request data, provided by a trigger. - * @param {string} data.message Message, provided by the trigger. + * @param {object} event The Cloud Functions event. + * @param {object} event.data The event data. + * @param {function} The callback function. */ -exports.helloWorld = function helloWorld (context, data) { - if (data.message === undefined) { - // This is an error case, "message" is required - context.failure('No message defined!'); +exports.helloWorld = function helloWorld (event, callback) { + if (!event.data.myMessage) { + // This is an error case, "myMessage" is required + callback(new Error('No message defined!')); } else { // Everything is ok - console.log(data.message); - context.success(); + console.log(event.data.myMessage); + callback(); } }; -// [END helloworld] - -// [START helloPromise] -var request = require('request-promise'); +// [END functions_background_helloworld] +// [START functions_background_promise] /** * Background Cloud Function that returns a Promise. Note that we don't pass - * a "context" argument to the function. + * a "callback" argument to the function. * - * @param {Object} data Request data, provided by a trigger. + * @param {object} event The Cloud Functions event. + * @param {object} event.data The event data. * @returns {Promise} */ -exports.helloPromise = function helloPromise (data) { +exports.helloPromise = function helloPromise (event) { + const request = require('request-promise'); + return request({ - uri: data.endpoint + uri: event.data.endpoint }); }; -// [END helloPromise] +// [END functions_background_promise] -// [START helloSynchronous] +// [START functions_background_synchronous] /** * Background Cloud Function that returns synchronously. Note that we don't pass - * a "context" argument to the function. + * a "callback" argument to the function. * - * @param {Object} data Request data, provided by a trigger. + * @param {object} event The Cloud Functions event. + * @param {object} event.data The event data. */ -exports.helloSynchronous = function helloSynchronous (data) { +exports.helloSynchronous = function helloSynchronous (event) { // This function returns synchronously - if (data.something === true) { + if (event.data.something === true) { return 'Something is true!'; } else { throw new Error('Something was not true!'); } }; -// [END helloSynchronous] +// [END functions_background_synchronous] diff --git a/functions/background/package.json b/functions/background/package.json index a3c09c4dc1..e9efbd14c6 100644 --- a/functions/background/package.json +++ b/functions/background/package.json @@ -6,12 +6,13 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- functions/background/test/*.test.js" }, "dependencies": { - "request-promise": "^3.0.0" + "request": "^2.78.0", + "request-promise": "^4.1.1" }, "devDependencies": { - "mocha": "^2.5.3" + "mocha": "^3.1.2" } } diff --git a/functions/background/test/index.test.js b/functions/background/test/index.test.js index ed992439a9..aab59f565d 100644 --- a/functions/background/test/index.test.js +++ b/functions/background/test/index.test.js @@ -1,26 +1,27 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var proxyquire = require('proxyquire').noCallThru(); +const proxyquire = require(`proxyquire`).noCallThru(); function getSample () { - var requestPromise = sinon.stub().returns(new Promise(function (resolve) { - resolve('test'); - })); + const requestPromise = sinon.stub().returns(Promise.resolve(`test`)); + return { - sample: proxyquire('../', { + program: proxyquire(`../`, { 'request-promise': requestPromise }), mocks: { @@ -29,62 +30,64 @@ function getSample () { }; } -function getMockContext () { - return { - success: sinon.stub(), - failure: sinon.stub() - }; -} +describe(`functions:background`, () => { + it(`should echo message`, () => { + const event = { + data: { + myMessage: `hi` + } + }; + const sample = getSample(); + const callback = sinon.stub(); -describe('functions:background', function () { - it('should echo message', function () { - var expectedMsg = 'hi'; - var context = getMockContext(); - var backgroundSample = getSample(); - backgroundSample.sample.helloWorld(context, { - message: expectedMsg - }); + sample.program.helloWorld(event, callback); - assert(context.success.calledOnce); - assert.equal(context.failure.called, false); - assert(console.log.calledWith(expectedMsg)); + assert.equal(console.log.callCount, 1); + assert.deepEqual(console.log.firstCall.args, [event.data.myMessage]); + assert.equal(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, []); }); - it('should say no message was provided', function () { - var expectedMsg = 'No message defined!'; - var context = getMockContext(); - var backgroundSample = getSample(); - backgroundSample.sample.helloWorld(context, {}); - assert(context.failure.calledOnce); - assert(context.failure.firstCall.args[0] === expectedMsg); - assert.equal(context.success.called, false); + it(`should say no message was provided`, () => { + const error = new Error(`No message defined!`); + const callback = sinon.stub(); + const sample = getSample(); + sample.program.helloWorld({ data: {} }, callback); + + assert.equal(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, [error]); }); - it('should make a promise request', function (done) { - var backgroundSample = getSample(); - backgroundSample.sample.helloPromise({ - endpoint: 'foo.com' - }).then(function (result) { - assert.deepEqual(backgroundSample.mocks.requestPromise.firstCall.args[0], { - uri: 'foo.com' + + it(`should make a promise request`, () => { + const sample = getSample(); + const event = { + data: { + endpoint: `foo.com` + } + }; + + return sample.program.helloPromise(event) + .then((result) => { + assert.deepEqual(sample.mocks.requestPromise.firstCall.args, [{ uri: `foo.com` }]); + assert.equal(result, `test`); }); - assert.equal(result, 'test'); - done(); - }, function () { - assert.fail(); - }); }); - it('should return synchronously', function () { - var backgroundSample = getSample(); - assert(backgroundSample.sample.helloSynchronous({ - something: true - }) === 'Something is true!'); + + it(`should return synchronously`, () => { + assert.equal(getSample().program.helloSynchronous({ + data: { + something: true + } + }), `Something is true!`); }); - it('should throw an error', function () { - var backgroundSample = getSample(); - assert.throws(function () { - backgroundSample.sample.helloSynchronous({ - something: false + + it(`should throw an error`, () => { + assert.throws(() => { + getSample().program.helloSynchronous({ + data: { + something: false + } }); - }, Error, 'Something was not true!'); + }, Error, `Something was not true!`); }); }); diff --git a/functions/datastore/README.md b/functions/datastore/README.md index aadbae89ee..7d63d15d1a 100644 --- a/functions/datastore/README.md +++ b/functions/datastore/README.md @@ -2,7 +2,8 @@ # Google Cloud Functions Cloud Datastore sample -This recipe shows you how to read and write an entity in Datastore from a Cloud Function. +This recipe shows you how to read and write an entity in Cloud Datastore from a +Cloud Function. View the [source code][code]. @@ -20,46 +21,69 @@ Functions for your project. 1. Create a Cloud Storage Bucket to stage our deployment: - gsutil mb gs://[YOUR_BUCKET_NAME] + gsutil mb gs://YOUR_BUCKET_NAME - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. + * Replace `YOUR_BUCKET_NAME` here and in subsequent commands with the name of your Cloud Storage Bucket. 1. Ensure the Cloud Datastore API is enabled: [Click here to enable the Cloud Datastore API](https://console.cloud.google.com/flows/enableapi?apiid=datastore.googleapis.com&redirect=https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/master/functions/datastore) -1. Deploy the "ds-get" function with an HTTP trigger: +1. Deploy the "get" function with an HTTP trigger: - gcloud alpha functions deploy ds-get --bucket [YOUR_BUCKET_NAME] --trigger-http --entry-point get + gcloud alpha functions deploy get --stage-bucket YOUR_BUCKET_NAME --trigger-http - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. +1. Deploy the "set" function with an HTTP trigger: -1. Deploy the "ds-set" function with an HTTP trigger: + gcloud alpha functions deploy set --stage-bucket YOUR_BUCKET_NAME --trigger-http - gcloud alpha functions deploy ds-set --bucket [YOUR_BUCKET_NAME] --trigger-http --entry-point set +1. Deploy the "del" function with an HTTP trigger: - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. + gcloud alpha functions deploy del --stage-bucket YOUR_BUCKET_NAME --trigger-http -1. Deploy the "ds-del" function with an HTTP trigger: +1. Call the "set" function to create a new entity: - gcloud alpha functions deploy ds-del --bucket [YOUR_BUCKET_NAME] --trigger-http --entry-point del + gcloud alpha functions call set --data '{"kind":"Task","key":"sampletask1","value":{"description":"Buy milk"}}' - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. + or -1. Call the "ds-set" function to create a new entity: + curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1","value":{"description":"Buy milk"}}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/set" - gcloud alpha functions call ds-set --data '{"kind":"gcf-test","key":"foobar","value":{"message":"Hello World!"}}' + * Replace `[YOUR_REGION]` with the region where your function is deployed. + * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. -1. Call the "ds-get" function to read the newly created entity: +1. Call the "get" function to read the newly created entity: - gcloud alpha functions call ds-get --data '{"kind":"gcf-test","key":"foobar"}' + gcloud alpha functions call get --data '{"kind":"Task","key":"sampletask1"}' -1. Call the "ds-del" function to delete the entity: + or - gcloud alpha functions call ds-del --data '{"kind":"gcf-test","key":"foobar"}' + curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1"}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/get" -1. Call the "ds-get" function again to verify it was deleted: + * Replace `[YOUR_REGION]` with the region where your function is deployed. + * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. + +1. Call the "del" function to delete the entity: + + gcloud alpha functions call del --data '{"kind":"Task","key":"sampletask1"}' + + or + + curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1"}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/del" + + * Replace `[YOUR_REGION]` with the region where your function is deployed. + * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. + +1. Call the "get" function again to verify it was deleted: + + gcloud alpha functions call get --data '{"kind":"Task","key":"sampletask1"}' + + or + + curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1"}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/get" + + * Replace `[YOUR_REGION]` with the region where your function is deployed. + * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. - gcloud alpha functions call ds-get --data '{"kind":"gcf-test","key":"foobar"}' [quickstart]: https://cloud.google.com/functions/quickstart diff --git a/functions/datastore/index.js b/functions/datastore/index.js index 9808bca5ea..11dff82e17 100644 --- a/functions/datastore/index.js +++ b/functions/datastore/index.js @@ -1,40 +1,40 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var Datastore = require('@google-cloud/datastore'); +const Datastore = require('@google-cloud/datastore'); -// Instantiate a datastore client -var datastore = Datastore(); +// Instantiates a client +const datastore = Datastore(); /** * Gets a Datastore key from the kind/key pair in the request. * - * @param {Object} requestData Cloud Function request data. + * @param {object} requestData Cloud Function request data. * @param {string} requestData.key Datastore key string. * @param {string} requestData.kind Datastore kind. - * @returns {Object} Datastore key object. + * @returns {object} Datastore key object. */ function getKeyFromRequestData (requestData) { if (!requestData.key) { - throw new Error('Key not provided. Make sure you have a "key" property ' + - 'in your request'); + throw new Error('Key not provided. Make sure you have a "key" property in your request'); } if (!requestData.kind) { - throw new Error('Kind not provided. Make sure you have a "kind" property ' + - 'in your request'); + throw new Error('Kind not provided. Make sure you have a "kind" property in your request'); } return datastore.key([requestData.kind, requestData.key]); @@ -44,111 +44,86 @@ function getKeyFromRequestData (requestData) { * Creates and/or updates a record. * * @example - * gcloud alpha functions call ds-set --data '{"kind":"gcf-test","key":"foobar","value":{"message": "Hello World!"}}' + * gcloud alpha functions call set --data '{"kind":"Task","key":"sampletask1","value":{"description": "Buy milk"}}' * - * @param {Object} context Cloud Function context. - * @param {Function} context.success Success callback. - * @param {Function} context.failure Failure callback. - * @param {Object} data Request data, in this case an object provided by the user. - * @param {string} data.kind The Datastore kind of the data to save, e.g. "user". - * @param {string} data.key Key at which to save the data, e.g. 5075192766267392. - * @param {Object} data.value Value to save to Cloud Datastore, e.g. {"name":"John"} + * @param {object} req Cloud Function request context. + * @param {object} req.body The request body. + * @param {string} req.body.kind The Datastore kind of the data to save, e.g. "Task". + * @param {string} req.body.key Key at which to save the data, e.g. "sampletask1". + * @param {object} req.body.value Value to save to Cloud Datastore, e.g. {"description":"Buy milk"} + * @param {object} res Cloud Function response context. */ -function set (context, data) { - try { - // The value contains a JSON document representing the entity we want to save - if (!data.value) { - throw new Error('Value not provided. Make sure you have a "value" ' + - 'property in your request'); - } - - var key = getKeyFromRequestData(data); - - return datastore.save({ - key: key, - data: data.value - }, function (err) { - if (err) { - console.error(err); - return context.failure(err); - } +exports.set = function set (req, res) { + // The value contains a JSON document representing the entity we want to save + if (!req.body.value) { + throw new Error('Value not provided. Make sure you have a "value" property in your request'); + } - return context.success('Entity saved'); + const key = getKeyFromRequestData(req.body); + const entity = { + key: key, + data: req.body.value + }; + + return datastore.save(entity) + .then(() => res.status(200).send(`Entity ${key.path.join('/')} saved.`)) + .catch((err) => { + console.error(err); + res.status(500).send(err); }); - } catch (err) { - console.error(err); - return context.failure(err.message); - } -} +}; /** * Retrieves a record. * * @example - * gcloud alpha functions call ds-get --data '{"kind":"gcf-test","key":"foobar"}' + * gcloud alpha functions call get --data '{"kind":"Task","key":"sampletask1"}' * - * @param {Object} context Cloud Function context. - * @param {Function} context.success Success callback. - * @param {Function} context.failure Failure callback. - * @param {Object} data Request data, in this case an object provided by the user. - * @param {string} data.kind The Datastore kind of the data to retrieve, e.g. "user". - * @param {string} data.key Key at which to retrieve the data, e.g. 5075192766267392. + * @param {object} req Cloud Function request context. + * @param {object} req.body The request body. + * @param {string} req.body.kind The Datastore kind of the data to retrieve, e.g. "Task". + * @param {string} req.body.key Key at which to retrieve the data, e.g. "sampletask1". + * @param {object} res Cloud Function response context. */ -function get (context, data) { - try { - var key = getKeyFromRequestData(data); - - return datastore.get(key, function (err, entity) { - if (err) { - console.error(err); - return context.failure(err); - } +exports.get = function get (req, res) { + const key = getKeyFromRequestData(req.body); + return datastore.get(key) + .then(([entity]) => { // The get operation will not fail for a non-existent entity, it just // returns null. if (!entity) { - return context.failure('No entity found for key ' + key.path); + throw new Error(`No entity found for key ${key.path.join('/')}.`); } - return context.success(entity); + res.status(200).send(entity); + }) + .catch((err) => { + console.error(err); + res.status(500).send(err); }); - } catch (err) { - console.error(err); - return context.failure(err.message); - } -} +}; /** * Deletes a record. * * @example - * gcloud alpha functions call ds-del --data '{"kind":"gcf-test","key":"foobar"}' + * gcloud alpha functions call del --data '{"kind":"Task","key":"sampletask1"}' * - * @param {Object} context Cloud Function context. - * @param {Function} context.success Success callback. - * @param {Function} context.failure Failure callback. - * @param {Object} data Request data, in this case an object provided by the user. - * @param {string} data.kind The Datastore kind of the data to delete, e.g. "user". - * @param {string} data.key Key at which to delete data, e.g. 5075192766267392. + * @param {object} req Cloud Function request context. + * @param {object} req.body The request body. + * @param {string} req.body.kind The Datastore kind of the data to delete, e.g. "Task". + * @param {string} req.body.key Key at which to delete data, e.g. "sampletask1". + * @param {object} res Cloud Function response context. */ -function del (context, data) { - try { - var key = getKeyFromRequestData(data); - - return datastore.delete(key, function (err) { - if (err) { - console.error(err); - return context.failure(err); - } - - return context.success('Entity deleted'); +exports.del = function del (req, res) { + const key = getKeyFromRequestData(req.body); + + // Deletes the entity + return datastore.delete(key) + .then(() => res.status(200).send(`Entity ${key.path.join('/')} deleted.`)) + .catch((err) => { + console.error(err); + res.status(500).send(err); }); - } catch (err) { - console.error(err); - return context.failure(err.message); - } -} - -exports.set = set; -exports.get = get; -exports.del = del; +}; diff --git a/functions/datastore/package.json b/functions/datastore/package.json index 089ed2a7c1..9ca43d860f 100644 --- a/functions/datastore/package.json +++ b/functions/datastore/package.json @@ -6,12 +6,9 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- functions/datastore/test/*.test.js" }, "dependencies": { - "@google-cloud/datastore": "^0.1.1" - }, - "devDependencies": { - "mocha": "^3.0.2" + "@google-cloud/datastore": "^0.5.0" } } diff --git a/functions/datastore/test/index.test.js b/functions/datastore/test/index.test.js index 41b86f3b4d..76894828cc 100644 --- a/functions/datastore/test/index.test.js +++ b/functions/datastore/test/index.test.js @@ -1,310 +1,266 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var proxyquire = require('proxyquire').noCallThru(); +const proxyquire = require(`proxyquire`).noCallThru(); -var KEY = 'key'; -var KIND = 'user'; +const NAME = `sampletask1`; +const KIND = `Task`; +const VALUE = { + description: `Buy milk` +}; function getSample () { - var datastore = { - delete: sinon.stub().callsArg(1), - get: sinon.stub().callsArg(1), - key: sinon.stub().returns({ - kind: KIND, - path: KEY - }), - save: sinon.stub().callsArg(1) + const key = { + kind: KIND, + name: NAME, + path: [KIND, NAME] + }; + const entity = { + key: key, + data: VALUE + }; + const datastore = { + delete: sinon.stub().returns(Promise.resolve()), + get: sinon.stub().returns(Promise.resolve([entity])), + key: sinon.stub().returns(key), + save: sinon.stub().returns(Promise.resolve()) }; - var DatastoreMock = sinon.stub().returns(datastore); + const DatastoreMock = sinon.stub().returns(datastore); + return { - sample: proxyquire('../', { + program: proxyquire(`../`, { '@google-cloud/datastore': DatastoreMock }), mocks: { Datastore: DatastoreMock, - datastore: datastore + datastore: datastore, + key: key, + entity: entity, + req: { + body: { + kind: KIND, + key: NAME, + value: VALUE + } + }, + res: { + status: sinon.stub().returnsThis(), + send: sinon.stub().returnsThis() + } } }; } -function getMockContext () { - return { - success: sinon.stub(), - failure: sinon.stub() - }; -} +describe(`functions:datastore`, () => { + it(`set: Set fails without a value`, () => { + const expectedMsg = `Value not provided. Make sure you have a "value" property in your request`; + const sample = getSample(); -describe('functions:datastore', function () { - it('set: Set fails without a value', function () { - var expectedMsg = 'Value not provided. Make sure you have a "value" ' + - 'property in your request'; - var context = getMockContext(); - - getSample().sample.set(context, {}); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); + assert.throws(() => { + sample.mocks.req.body.value = undefined; + sample.program.set(sample.mocks.req, sample.mocks.res); + }, Error, expectedMsg); }); - it('set: Set fails without a key', function () { - var expectedMsg = 'Key not provided. Make sure you have a "key" ' + - 'property in your request'; - var context = getMockContext(); - - getSample().sample.set(context, { - value: {} - }); + it(`set: Set fails without a key`, () => { + const expectedMsg = `Key not provided. Make sure you have a "key" property in your request`; + const sample = getSample(); - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); + assert.throws(() => { + sample.mocks.req.body.key = undefined; + sample.program.set(sample.mocks.req, sample.mocks.res); + }, Error, expectedMsg); }); - it('set: Set fails without a kind', function () { - var expectedMsg = 'Kind not provided. Make sure you have a "kind" ' + - 'property in your request'; - var context = getMockContext(); + it(`set: Set fails without a kind`, () => { + const expectedMsg = `Kind not provided. Make sure you have a "kind" property in your request`; + const sample = getSample(); - getSample().sample.set(context, { - value: {}, - key: KEY - }); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); + assert.throws(() => { + sample.mocks.req.body.kind = undefined; + sample.program.set(sample.mocks.req, sample.mocks.res); + }, Error, expectedMsg); }); - it('set: Handles save error', function () { - var expectedMsg = 'test error'; - var context = getMockContext(); - var datastoreSample = getSample(); - - datastoreSample.mocks.datastore.save = sinon.stub().callsArgWith( - 1, - expectedMsg - ); - - datastoreSample.sample.set(context, { - value: {}, - key: KEY, - kind: KIND - }); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(datastoreSample.mocks.datastore.save.calledOnce, true); + it(`set: Handles save error`, () => { + const error = new Error(`error`); + const sample = getSample(); + + sample.mocks.datastore.save.returns(Promise.reject(error)); + + return sample.program.set(sample.mocks.req, sample.mocks.res) + .then(() => { + throw new Error(`Should have failed!`); + }) + .catch((err) => { + assert.deepEqual(err, error); + assert.deepEqual(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + assert.deepEqual(sample.mocks.res.status.callCount, 1); + assert.deepEqual(sample.mocks.res.status.firstCall.args, [500]); + assert.deepEqual(sample.mocks.res.send.callCount, 1); + assert.deepEqual(sample.mocks.res.send.firstCall.args, [error]); + }); }); - it('set: Set saves an entity', function () { - var expectedMsg = 'Entity saved'; - var context = getMockContext(); - var datastoreSample = getSample(); - - var data = { - value: { - name: 'John' - }, - key: KEY, - kind: KIND - }; - - datastoreSample.sample.set(context, data); - - assert.equal(context.success.calledOnce, true); - assert.equal(context.success.firstCall.args[0], expectedMsg); - assert.equal(context.failure.called, false); - assert.equal(datastoreSample.mocks.datastore.key.calledOnce, true); - assert.deepEqual( - datastoreSample.mocks.datastore.key.firstCall.args[0], - [data.kind, data.key] - ); - assert.equal(datastoreSample.mocks.datastore.save.calledOnce, true); - assert.deepEqual(datastoreSample.mocks.datastore.save.firstCall.args[0], { - key: { - kind: data.kind, - path: data.key - }, - data: data.value - }); + it(`set: Set saves an entity`, () => { + const expectedMsg = `Entity ${KIND}/${NAME} saved.`; + const sample = getSample(); + + return sample.program.set(sample.mocks.req, sample.mocks.res) + .then(() => { + assert.deepEqual(sample.mocks.datastore.save.callCount, 1); + assert.deepEqual(sample.mocks.datastore.save.firstCall.args, [sample.mocks.entity]); + assert.deepEqual(sample.mocks.res.status.callCount, 1); + assert.deepEqual(sample.mocks.res.status.firstCall.args, [200]); + assert.deepEqual(sample.mocks.res.send.callCount, 1); + assert.deepEqual(sample.mocks.res.send.firstCall.args, [expectedMsg]); + }); }); - it('get: Get fails without a key', function () { - var expectedMsg = 'Key not provided. Make sure you have a "key" ' + - 'property in your request'; - var context = getMockContext(); - - getSample().sample.get(context, {}); + it(`get: Get fails without a key`, () => { + const expectedMsg = `Key not provided. Make sure you have a "key" property in your request`; + const sample = getSample(); - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); + assert.throws(() => { + sample.mocks.req.body.key = undefined; + sample.program.get(sample.mocks.req, sample.mocks.res); + }, Error, expectedMsg); }); - it('get: Get fails without a kind', function () { - var expectedMsg = 'Kind not provided. Make sure you have a "kind" ' + - 'property in your request'; - var context = getMockContext(); - - getSample().sample.get(context, { - key: KEY - }); + it(`get: Get fails without a kind`, () => { + const expectedMsg = `Kind not provided. Make sure you have a "kind" property in your request`; + const sample = getSample(); - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); + assert.throws(() => { + sample.mocks.req.body.kind = undefined; + sample.program.get(sample.mocks.req, sample.mocks.res); + }, Error, expectedMsg); }); - it('get: Handles get error', function () { - var expectedMsg = 'test error'; - var context = getMockContext(); - var datastoreSample = getSample(); - - datastoreSample.mocks.datastore.get = sinon.stub().callsArgWith( - 1, - expectedMsg - ); - - datastoreSample.sample.get(context, { - key: KEY, - kind: KIND - }); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(datastoreSample.mocks.datastore.get.calledOnce, true); + it(`get: Handles get error`, () => { + const error = new Error(`error`); + const sample = getSample(); + + sample.mocks.datastore.get.returns(Promise.reject(error)); + + return sample.program.get(sample.mocks.req, sample.mocks.res) + .then(() => { + throw new Error(`Should have failed!`); + }) + .catch((err) => { + assert.deepEqual(err, error); + assert.deepEqual(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + assert.deepEqual(sample.mocks.res.status.callCount, 1); + assert.deepEqual(sample.mocks.res.status.firstCall.args, [500]); + assert.deepEqual(sample.mocks.res.send.callCount, 1); + assert.deepEqual(sample.mocks.res.send.firstCall.args, [error]); + }); }); - it('get: Fails when entity does not exist', function () { - var expectedMsg = 'No entity found for key key'; - var context = getMockContext(); - var datastoreSample = getSample(); - - datastoreSample.sample.get(context, { - key: KEY, - kind: KIND - }); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(datastoreSample.mocks.datastore.get.calledOnce, true); + it(`get: Fails when entity does not exist`, () => { + const sample = getSample(); + const error = new Error(`No entity found for key ${sample.mocks.key.path.join('/')}.`); + + sample.mocks.datastore.get.returns(Promise.resolve([])); + + return sample.program.get(sample.mocks.req, sample.mocks.res) + .then(() => { + throw new Error(`Should have failed!`); + }) + .catch((err) => { + assert.deepEqual(err, error); + assert.deepEqual(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + assert.deepEqual(sample.mocks.res.status.callCount, 1); + assert.deepEqual(sample.mocks.res.status.firstCall.args, [500]); + assert.deepEqual(sample.mocks.res.send.callCount, 1); + assert.deepEqual(sample.mocks.res.send.firstCall.args, [error]); + }); }); - it('get: Finds an entity', function () { - var expectedResult = { - name: 'John' - }; - var context = getMockContext(); - var datastoreSample = getSample(); - - datastoreSample.mocks.datastore.get = sinon.stub().callsArgWith( - 1, - null, - expectedResult - ); - datastoreSample.sample.get(context, { - key: KEY, - kind: KIND - }); - - assert.equal(context.success.calledOnce, true); - assert.equal(context.success.firstCall.args[0], expectedResult); - assert.equal(context.failure.called, false); - assert.equal(datastoreSample.mocks.datastore.get.calledOnce, true); - assert.deepEqual( - datastoreSample.mocks.datastore.get.firstCall.args[0], - { - path: KEY, - kind: KIND - } - ); + it(`get: Finds an entity`, () => { + const sample = getSample(); + + return sample.program.get(sample.mocks.req, sample.mocks.res) + .then(() => { + assert.deepEqual(sample.mocks.datastore.get.callCount, 1); + assert.deepEqual(sample.mocks.datastore.get.firstCall.args, [sample.mocks.key]); + assert.deepEqual(sample.mocks.res.status.callCount, 1); + assert.deepEqual(sample.mocks.res.status.firstCall.args, [200]); + assert.deepEqual(sample.mocks.res.send.callCount, 1); + assert.deepEqual(sample.mocks.res.send.firstCall.args, [sample.mocks.entity]); + }); }); - it('del: Delete fails without a key', function () { - var expectedMsg = 'Key not provided. Make sure you have a "key" ' + - 'property in your request'; - var context = getMockContext(); + it(`del: Delete fails without a key`, () => { + const expectedMsg = `Key not provided. Make sure you have a "key" property in your request`; + const sample = getSample(); - getSample().sample.del(context, {}); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); + assert.throws(() => { + sample.mocks.req.body.key = undefined; + sample.program.del(sample.mocks.req, sample.mocks.res); + }, Error, expectedMsg); }); - it('del: Delete fails without a kind', function () { - var expectedMsg = 'Kind not provided. Make sure you have a "kind" ' + - 'property in your request'; - var context = getMockContext(); - - getSample().sample.del(context, { - key: KEY - }); + it(`del: Delete fails without a kind`, () => { + const expectedMsg = `Kind not provided. Make sure you have a "kind" property in your request`; + const sample = getSample(); - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); + assert.throws(() => { + sample.mocks.req.body.kind = undefined; + sample.program.del(sample.mocks.req, sample.mocks.res); + }, Error, expectedMsg); }); - it('del: Handles delete error', function () { - var expectedMsg = 'Kind not provided. Make sure you have a "kind" ' + - 'property in your request'; - var context = getMockContext(); - var datastoreSample = getSample(); - - datastoreSample.mocks.datastore.delete = sinon.stub().callsArgWith( - 1, - expectedMsg - ); - - datastoreSample.sample.del(context, { - key: KEY, - kind: KIND - }); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(datastoreSample.mocks.datastore.delete.calledOnce, true); + it(`del: Handles delete error`, () => { + const error = new Error(`error`); + const sample = getSample(); + + sample.mocks.datastore.delete.returns(Promise.reject(error)); + + return sample.program.del(sample.mocks.req, sample.mocks.res) + .then(() => { + throw new Error(`Should have failed!`); + }) + .catch((err) => { + assert.deepEqual(err, error); + assert.deepEqual(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + assert.deepEqual(sample.mocks.res.status.callCount, 1); + assert.deepEqual(sample.mocks.res.status.firstCall.args, [500]); + assert.deepEqual(sample.mocks.res.send.callCount, 1); + assert.deepEqual(sample.mocks.res.send.firstCall.args, [error]); + }); }); - it('del: Deletes an entity', function () { - var expectedMsg = 'Entity deleted'; - var context = getMockContext(); - var datastoreSample = getSample(); - - datastoreSample.sample.del(context, { - key: KEY, - kind: KIND - }); - - assert.equal(context.success.calledOnce, true); - assert.equal(context.success.firstCall.args[0], expectedMsg); - assert.equal(context.failure.called, false); - assert.equal(datastoreSample.mocks.datastore.delete.calledOnce, true); - assert.deepEqual( - datastoreSample.mocks.datastore.delete.firstCall.args[0], - { - path: KEY, - kind: KIND - } - ); + it(`del: Deletes an entity`, () => { + const expectedMsg = `Entity ${KIND}/${NAME} deleted.`; + const sample = getSample(); + + return sample.program.del(sample.mocks.req, sample.mocks.res) + .then(() => { + assert.deepEqual(sample.mocks.datastore.delete.callCount, 1); + assert.deepEqual(sample.mocks.datastore.delete.firstCall.args, [sample.mocks.key]); + assert.deepEqual(sample.mocks.res.status.callCount, 1); + assert.deepEqual(sample.mocks.res.status.firstCall.args, [200]); + assert.deepEqual(sample.mocks.res.send.callCount, 1); + assert.deepEqual(sample.mocks.res.send.firstCall.args, [expectedMsg]); + }); }); }); diff --git a/functions/errorreporting/index.js b/functions/errorreporting/index.js index c02c4cf726..1913fe32c5 100644 --- a/functions/errorreporting/index.js +++ b/functions/errorreporting/index.js @@ -1,30 +1,30 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -// [START setup] -var Logging = require('@google-cloud/logging'); +// [START functions_errorreporting_setup] +const Logging = require('@google-cloud/logging'); -// Instantiate a logging client -var logging = Logging(); -// [END setup] +// Instantiates a client +const logging = Logging(); +// [END functions_errorreporting_setup] -// [START reportDetailedError] -var reportDetailedError = require('./report'); -// [END reportDetailedError] +const reportDetailedError = require('./report'); -// [START helloSimpleErrorReport] +// [START functions_errorreporting_report] /** * Report an error to StackDriver Error Reporting. Writes the minimum data * required for the error to be picked up by StackDriver Error Reporting. @@ -36,106 +36,114 @@ function reportError (err, callback) { // This is the name of the StackDriver log stream that will receive the log // entry. This name can be any valid log stream name, but must contain "err" // in order for the error to be picked up by StackDriver Error Reporting. - var logName = 'errors'; - var log = logging.log(logName); + const logName = 'errors'; + const log = logging.log(logName); - // https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/MonitoredResource - var monitoredResource = { - type: 'cloud_function', - labels: { - function_name: process.env.FUNCTION_NAME + const metadata = { + // https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/MonitoredResource + resource: { + type: 'cloud_function', + labels: { + function_name: process.env.FUNCTION_NAME + } } }; // https://cloud.google.com/error-reporting/reference/rest/v1beta1/ErrorEvent - var errorEvent = { + const errorEvent = { message: err.stack, serviceContext: { - service: 'cloud_function:' + process.env.FUNCTION_NAME, + service: `cloud_function:${process.env.FUNCTION_NAME}`, version: require('./package.json').version || 'unknown' } }; // Write the error log entry - log.write(log.entry(monitoredResource, errorEvent), callback); + log.write(log.entry(metadata, errorEvent), callback); } -// [END helloSimpleErrorReport] +// [END functions_errorreporting_report] -// [START helloSimpleError] +// [START functions_errorreporting_simple] /** * HTTP Cloud Function. * - * @param {Object} req Cloud Function request object. - * @param {Object} res Cloud Function response object. + * @param {object} req Cloud Function request context. + * @param {object} res Cloud Function response context. */ exports.helloSimpleError = function helloSimpleError (req, res) { try { if (req.method !== 'GET') { - var error = new Error('Only GET requests are accepted!'); + const error = new Error('Only GET requests are accepted!'); error.code = 405; throw error; } // All is good, respond to the HTTP request - return res.send('Hello World!'); + res.send('Hello World!'); } catch (err) { // Report the error - return reportError(err, function () { + reportError(err, () => { // Now respond to the HTTP request - return res.status(error.code || 500).send(err.message); + res.status(err.code || 500).send(err.message); }); } }; -// [END helloSimpleError] +// [END functions_errorreporting_simple] -// [START helloHttpError] +// [START functions_errorreporting_http] /** * HTTP Cloud Function. * - * @param {Object} req Cloud Function request object. - * @param {Object} res Cloud Function response object. + * @param {object} req Cloud Function request context. + * @param {object} req.body The request body. + * @param {string} req.body.message Message provided in the request. + * @param {object} res Cloud Function response context. */ exports.helloHttpError = function helloHttpError (req, res) { try { if (req.method !== 'POST' && req.method !== 'GET') { - var error = new Error('Only POST and GET requests are accepted!'); + const error = new Error('Only POST and GET requests are accepted!'); error.code = 405; throw error; } // All is good, respond to the HTTP request - return res.send('Hello ' + (req.body.message || 'World') + '!'); + res.send(`Hello ${req.body.message || 'World'}!`); } catch (err) { // Set the response status code before reporting the error res.status(err.code || 500); // Report the error - return reportDetailedError(err, req, res, function () { + reportDetailedError(err, req, res, () => { // Now respond to the HTTP request - return res.send(err.message); + res.send(err); }); } }; -// [END helloHttpError] +// [END functions_errorreporting_http] -// [START helloBackgroundError] +// [START functions_errorreporting_background] /** * Background Cloud Function. * - * @param {Object} context Cloud Function context object. - * @param {Object} data Request data, provided by a trigger. - * @param {string} data.message Message, provided by the trigger. + * @param {object} event The Cloud Functions event. + * @param {object} event.data The event data. + * @param {string} event.data.message Message, provided by the trigger. + * @param {function} The callback function. */ -exports.helloBackgroundError = function helloBackgroundError (context, data) { +exports.helloBackgroundError = function helloBackgroundError (event, callback) { try { - if (!data.message) { + if (!event.data.message) { throw new Error('"message" is required!'); } - // All is good, respond with a message - return context.success('Hello World!'); + + // Do something + + // Done, respond with a success message + callback(null, 'Done!'); } catch (err) { // Report the error - return reportDetailedError(err, function () { - // Now finish mark the execution failure - return context.failure(err.message); + reportDetailedError(err, () => { + // Now finish and mark the execution as a failure + callback(err); }); } }; -// [END helloBackgroundError] +// [END functions_errorreporting_background] diff --git a/functions/errorreporting/package.json b/functions/errorreporting/package.json index 7079b6eb85..298f5e1cfa 100644 --- a/functions/errorreporting/package.json +++ b/functions/errorreporting/package.json @@ -6,12 +6,9 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- functions/errorreporting/test/*.test.js" }, "dependencies": { - "@google-cloud/logging": "^0.1.1" - }, - "devDependencies": { - "mocha": "^3.0.2" + "@google-cloud/logging": "^0.5.0" } } diff --git a/functions/errorreporting/report.js b/functions/errorreporting/report.js index 9338ec92f5..6bfbe6f154 100644 --- a/functions/errorreporting/report.js +++ b/functions/errorreporting/report.js @@ -1,33 +1,35 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var Logging = require('@google-cloud/logging'); +const Logging = require('@google-cloud/logging'); -// Instantiate a logging client -var logging = Logging(); +// Instantiates a client +const logging = Logging(); -// [START helloHttpError] +// [START functions_errorreporting_report_advanced] /** * Report an error to StackDriver Error Reporting. Writes up to the maximum data * accepted by StackDriver Error Reporting. * * @param {Error} err The Error object to report. - * @param {Object} [req] Request context, if any. - * @param {Object} [res] Response context, if any. - * @param {Object} [options] Additional context, if any. - * @param {Function} callback Callback function. + * @param {object} [req] Request context, if any. + * @param {object} [res] Response context, if any. + * @param {object} [options] Additional context, if any. + * @param {function} callback Callback function. */ function reportDetailedError (err, req, res, options, callback) { if (typeof req === 'function') { @@ -41,27 +43,30 @@ function reportDetailedError (err, req, res, options, callback) { } options || (options = {}); - var FUNCTION_NAME = process.env.FUNCTION_NAME; - var log = logging.log('errors'); + const FUNCTION_NAME = process.env.FUNCTION_NAME; + const log = logging.log('errors'); - // MonitoredResource - // See https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/MonitoredResource - var resource = { - // MonitoredResource.type - type: 'cloud_function', - // MonitoredResource.labels - labels: { - function_name: FUNCTION_NAME + const metadata = { + // MonitoredResource + // See https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/MonitoredResource + resource: { + // MonitoredResource.type + type: 'cloud_function', + // MonitoredResource.labels + labels: { + function_name: FUNCTION_NAME + } } }; + if (typeof options.region === 'string') { - resource.labels.region = options.region; + metadata.resource.labels.region = options.region; } if (typeof options.projectId === 'string') { - resource.labels.projectId = options.projectId; + metadata.resource.labels.projectId = options.projectId; } - var context = {}; + const context = {}; if (typeof options.user === 'string') { // ErrorEvent.context.user context.user = options.user; @@ -90,7 +95,7 @@ function reportDetailedError (err, req, res, options, callback) { try { if (options.version === undefined) { - var pkg = require('./package.json'); + const pkg = require('./package.json'); options.version = pkg.version; } } catch (err) {} @@ -100,13 +105,13 @@ function reportDetailedError (err, req, res, options, callback) { // ErrorEvent // See https://cloud.google.com/error-reporting/reference/rest/v1beta1/ErrorEvent - var structPayload = { + const structPayload = { // ErrorEvent.serviceContext serviceContext: { // ErrorEvent.serviceContext.service - service: 'cloud_function:' + FUNCTION_NAME, + service: `cloud_function:${FUNCTION_NAME}`, // ErrorEvent.serviceContext.version - version: '' + options.version + version: `${options.version}` }, // ErrorEvent.context context: context @@ -121,8 +126,8 @@ function reportDetailedError (err, req, res, options, callback) { structPayload.message = err.message; } - log.write(log.entry(resource, structPayload), callback); + log.write(log.entry(metadata, structPayload), callback); } -// [END helloHttpError] +// [END functions_errorreporting_report_advanced] module.exports = reportDetailedError; diff --git a/functions/errorreporting/test/index.test.js b/functions/errorreporting/test/index.test.js index f6b0ffc851..d82087a3db 100644 --- a/functions/errorreporting/test/index.test.js +++ b/functions/errorreporting/test/index.test.js @@ -1,18 +1,20 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -describe('functions:errorreporting', function () { - it('should have tests'); +describe(`functions:errorreporting`, () => { + it(`should have tests`); }); diff --git a/functions/gcs/README.md b/functions/gcs/README.md index ada491d689..1df79f4a49 100644 --- a/functions/gcs/README.md +++ b/functions/gcs/README.md @@ -20,27 +20,27 @@ Functions for your project. 1. Create a Cloud Storage Bucket to stage our deployment: - gsutil mb gs://[YOUR_BUCKET_NAME] + gsutil mb gs://YOUR_BUCKET_NAME - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. + * Replace `YOUR_BUCKET_NAME` with the name of your Cloud Storage Bucket. 1. Upload the sample file to the bucket: - gsutil cp sample.txt gs://[YOUR_BUCKET_NAME] + gsutil cp sample.txt gs://YOUR_BUCKET_NAME - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. + * Replace `YOUR_BUCKET_NAME` with the name of your Cloud Storage Bucket. 1. Deploy the "wordCount" function with an HTTP trigger: - gcloud alpha functions deploy wordCount --bucket [YOUR_BUCKET_NAME] --trigger-http --entry-point map + gcloud alpha functions deploy wordCount --stage-bucket YOUR_BUCKET_NAME --trigger-http - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. + * Replace `YOUR_BUCKET_NAME` with the name of your Cloud Storage Bucket. 1. Call the "wordCount" function using the sample file: - gcloud alpha functions call wordCount --data '{"bucket":"[YOUR_BUCKET_NAME]","file":"sample.txt"}' + gcloud alpha functions call wordCount --data '{"bucket":"YOUR_BUCKET_NAME","file":"sample.txt"}' - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. + * Replace `YOUR_BUCKET_NAME` with the name of your Cloud Storage Bucket. You should see something like this in your console diff --git a/functions/gcs/index.js b/functions/gcs/index.js index 0610de3fb2..46588a3597 100644 --- a/functions/gcs/index.js +++ b/functions/gcs/index.js @@ -1,70 +1,73 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var Storage = require('@google-cloud/storage'); +// [START functions_word_count_setup] +const Storage = require('@google-cloud/storage'); +const readline = require('readline'); -var readline = require('readline'); +// Instantiates a client +const storage = Storage(); +// [END functions_word_count_setup] -function getFileStream (bucketName, fileName) { - if (!bucketName) { - throw new Error('Bucket not provided. Make sure you have a ' + - '"bucket" property in your request'); +function getFileStream (file) { + if (!file.bucket) { + throw new Error('Bucket not provided. Make sure you have a "bucket" property in your request'); } - if (!fileName) { - throw new Error('Filename not provided. Make sure you have a ' + - '"file" property in your request'); + if (!file.name) { + throw new Error('Filename not provided. Make sure you have a "name" property in your request'); } - // Instantiate a storage client - var storage = Storage(); - var bucket = storage.bucket(bucketName); - return bucket.file(fileName).createReadStream(); + return storage.bucket(file.bucket).file(file.name).createReadStream(); } +// [START functions_word_count_read] /** * Reads file and responds with the number of words in the file. * * @example - * gcloud alpha functions call wordCount --data '{"bucket":"","file":"sample.txt"}' + * gcloud alpha functions call wordCount --data '{"bucket":"YOUR_BUCKET_NAME","file":"sample.txt"}' * - * @param {Object} context Cloud Function context. - * @param {Function} context.success Success callback. - * @param {Function} context.failure Failure callback. - * @param {Object} data Request data, in this case an object provided by the user. - * @param {Object} data.bucket Name of a Cloud Storage bucket. - * @param {Object} data.file Name of a file in the Cloud Storage bucket. + * @param {object} event The Cloud Functions event. + * @param {object} event.data A Google Cloud Storage File object. + * @param {string} event.data.bucket Name of a Cloud Storage bucket. + * @param {string} event.data.name Name of a file in the Cloud Storage bucket. + * @param {function} The callback function. */ -function wordCount (context, data) { - try { - var count = 0; +exports.wordCount = function (event, callback) { + const file = event.data; - // Use the linebyline module to read the stream line by line. - var lineReader = readline.createInterface({ - input: getFileStream(data.bucket, data.file) - }); + if (file.resourceState === 'not_exists') { + // This is a file deletion event, so skip it + callback(); + return; + } - lineReader.on('line', function (line) { - count += line.trim().split(/\s+/).length; - }); + let count = 0; + const options = { + input: getFileStream(file) + }; - lineReader.on('close', function () { - context.success('The file ' + data.file + ' has ' + count + ' words'); + // Use the readline module to read the stream line by line. + readline.createInterface(options) + .on('line', (line) => { + count += line.trim().split(/\s+/).length; + }) + .on('close', () => { + callback(null, `File ${file.name} has ${count} words`); }); - } catch (err) { - context.failure(err.message); - } -} - -exports.wordCount = wordCount; +}; +// [END functions_word_count_read] diff --git a/functions/gcs/package.json b/functions/gcs/package.json index 0349fdacc7..3d5f945f44 100644 --- a/functions/gcs/package.json +++ b/functions/gcs/package.json @@ -6,12 +6,9 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" - }, - "dependencies": { - "@google-cloud/storage": "^0.1.1" - }, - "devDependencies": { - "mocha": "^3.0.2" + "test": "cd ..; npm run t -- functions/gcs/test/*.test.js" + }, "dependencies": { + "@google-cloud/storage": "^0.4.0", + "request": "^2.78.0" } } diff --git a/functions/gcs/test/index.test.js b/functions/gcs/test/index.test.js index c05b9e2fb2..49a2ca18b7 100644 --- a/functions/gcs/test/index.test.js +++ b/functions/gcs/test/index.test.js @@ -1,38 +1,40 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var fs = require('fs'); -var path = require('path'); -var proxyquire = require('proxyquire').noCallThru(); +const fs = require(`fs`); +const path = require(`path`); +const proxyquire = require(`proxyquire`).noCallThru(); +const filename = `sample.txt`; function getSample () { - var file = { - createReadStream: function () { - var filepath = path.join(__dirname, '../sample.txt'); - return fs.createReadStream(filepath, { encoding: 'utf8' }); - } + const filePath = path.join(__dirname, `../${filename}`); + const file = { + createReadStream: () => fs.createReadStream(filePath, { encoding: `utf8` }) }; - var bucket = { + const bucket = { file: sinon.stub().returns(file) }; - var storage = { + const storage = { bucket: sinon.stub().returns(bucket) }; - var StorageMock = sinon.stub().returns(storage); + const StorageMock = sinon.stub().returns(storage); + return { - sample: proxyquire('../', { + program: proxyquire(`../`, { '@google-cloud/storage': StorageMock }), mocks: { @@ -44,64 +46,62 @@ function getSample () { }; } -function getMockContext () { - return { - success: sinon.stub(), - failure: sinon.stub() - }; -} +describe(`functions:gcs`, () => { + it(`Fails without a bucket`, () => { + const expectedMsg = `Bucket not provided. Make sure you have a "bucket" property in your request`; -describe('functions:gcs', function () { - it('Fails without a bucket', function () { - var expectedMsg = 'Bucket not provided. Make sure you have a "bucket" ' + - 'property in your request'; - var context = getMockContext(); + assert.throws( + () => getSample().program.wordCount({ data: { name: `file` } }), + Error, + expectedMsg + ); + }); - getSample().sample.wordCount(context, { - file: 'file' - }); + it(`Fails without a file`, () => { + const expectedMsg = `Filename not provided. Make sure you have a "file" property in your request`; - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); + assert.throws( + () => getSample().program.wordCount({ data: { bucket: `bucket` } }), + Error, + expectedMsg + ); }); - it('Fails without a file', function () { - var expectedMsg = 'Filename not provided. Make sure you have a "file" ' + - 'property in your request'; - var context = getMockContext(); + it(`Does nothing for deleted files`, (done) => { + const event = { + data: { + resourceState: `not_exists` + } + }; + const sample = getSample(); - getSample().sample.wordCount(context, { - bucket: 'bucket' + sample.program.wordCount(event, (err, message) => { + assert.ifError(err); + assert.equal(message, undefined); + assert.deepEqual(sample.mocks.storage.bucket.callCount, 0); + assert.deepEqual(sample.mocks.bucket.file.callCount, 0); + done(); }); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); }); - it('Reads the file line by line', function (done) { - var expectedMsg = 'The file sample.txt has 114 words'; - var data = { - bucket: 'bucket', - file: 'sample.txt' - }; - var context = { - success: function (message) { - assert.equal(message, expectedMsg); - done(); - }, - failure: function () { - done('Should have succeeded!'); + it(`Reads the file line by line`, (done) => { + const expectedMsg = `File ${filename} has 114 words`; + const event = { + data: { + bucket: `bucket`, + name: `sample.txt` } }; - var gcsSample = getSample(); - gcsSample.sample.wordCount(context, data); - - assert.equal(gcsSample.mocks.storage.bucket.calledOnce, true); - assert.equal(gcsSample.mocks.storage.bucket.firstCall.args[0], data.bucket); - assert.equal(gcsSample.mocks.bucket.file.calledOnce, true); - assert.equal(gcsSample.mocks.bucket.file.firstCall.args[0], data.file); + const sample = getSample(); + sample.program.wordCount(event, (err, message) => { + assert.ifError(err); + assert.deepEqual(message, expectedMsg); + assert.deepEqual(sample.mocks.storage.bucket.calledOnce, true); + assert.deepEqual(sample.mocks.storage.bucket.firstCall.args, [event.data.bucket]); + assert.deepEqual(sample.mocks.bucket.file.calledOnce, true); + assert.deepEqual(sample.mocks.bucket.file.firstCall.args, [event.data.name]); + done(); + }); }); }); diff --git a/functions/helloworld/index.js b/functions/helloworld/index.js index 9b94f2ab34..9eafc98e84 100644 --- a/functions/helloworld/index.js +++ b/functions/helloworld/index.js @@ -1,32 +1,38 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -// [START helloworld] +// [START functions_helloworld_debug] +require('@google/cloud-debug').start(); +// [END functions_helloworld_debug] + +// [START functions_helloworld] /** * Cloud Function. * - * @param {Object} context Cloud Function context. - * @param {Object} data Request data, provided by a trigger. + * @param {object} event The Cloud Functions event. + * @param {function} The callback function. */ -exports.helloWorld = function helloWorld (context, data) { - console.log('My Cloud Function: ' + data.message); - context.success(); +exports.helloWorld = function helloWorld (event, callback) { + console.log(`My Cloud Function: ${event.data.message}`); + callback(); }; -// [END helloworld] +// [END functions_helloworld] -// [START helloGET] +// [START functions_helloworld_get] /** * HTTP Cloud Function. * @@ -36,9 +42,9 @@ exports.helloWorld = function helloWorld (context, data) { exports.helloGET = function helloGET (req, res) { res.send('Hello World!'); }; -// [END helloGET] +// [END functions_helloworld_get] -// [START helloHttp] +// [START functions_helloworld_http] /** * HTTP Cloud Function. * @@ -46,44 +52,95 @@ exports.helloGET = function helloGET (req, res) { * @param {Object} res Cloud Function response context. */ exports.helloHttp = function helloHttp (req, res) { - res.send('Hello ' + (req.body.name || 'World') + '!'); + res.send(`Hello ${req.body.name || 'World'}!`); }; -// [END helloHttp] +// [END functions_helloworld_http] -// [START helloBackground] +// [START functions_helloworld_background] /** * Background Cloud Function. * - * @param {Object} context Cloud Function context. - * @param {Object} data Request data, provided by a trigger. + * @param {object} event The Cloud Functions event. + * @param {function} The callback function. */ -exports.helloBackground = function helloBackground (context, data) { - context.success('Hello ' + (data.name || 'World') + '!'); +exports.helloBackground = function helloBackground (event, callback) { + callback(null, `Hello ${event.data.name || 'World'}!`); }; -// [END helloBackground] +// [END functions_helloworld_background] -// [START helloPubSub] +// [START functions_helloworld_pubsub] /** * Background Cloud Function to be triggered by Pub/Sub. * - * @param {Object} context Cloud Function context. - * @param {Object} data Request data, provided by a Pub/Sub trigger. + * @param {object} event The Cloud Functions event. + * @param {function} The callback function. */ -exports.helloPubSub = function helloPubSub (context, data) { - console.log('Hello ' + (data.name || 'World') + '!'); - context.success(); +exports.helloPubSub = function helloPubSub (event, callback) { + const pubsubMessage = event.data; + const name = pubsubMessage.data ? Buffer.from(pubsubMessage.data, 'base64').toString() : 'World'; + console.log(`Hello ${name}!`); + callback(); }; -// [END helloPubSub] +// [END functions_helloworld_pubsub] -// [START helloGCS] +// [START functions_helloworld_storage] /** * Background Cloud Function to be triggered by Cloud Storage. * - * @param {Object} context Cloud Function context. - * @param {Object} data Request data, provided by a Cloud Storage trigger. + * @param {object} event The Cloud Functions event. + * @param {function} The callback function. + */ +exports.helloGCS = function helloGCS (event, callback) { + const file = event.data; + const isDelete = file.resourceState === 'not_exists'; + + if (isDelete) { + console.log(`File ${file.name} deleted.`); + } else { + console.log(`File ${file.name} uploaded.`); + } + + callback(); +}; +// [END functions_helloworld_storage] + +// [START functions_helloworld_error] +/** + * Background Cloud Function that throws an error. + * + * @param {object} event The Cloud Functions event. + * @param {function} The callback function. + */ +exports.helloError = function helloError (event, callback) { + // This WILL be reported to Stackdriver errors + throw new Error('I failed you'); +}; +// [END functions_helloworld_error] + +/* eslint-disable */ +// [START functions_helloworld_error_2] +/** + * Background Cloud Function that throws a value. + * + * @param {object} event The Cloud Functions event. + * @param {function} The callback function. + */ +exports.helloError2 = function helloError2 (event, callback) { + // This will NOT be reported to Stackdriver errors + throw 1; +}; +// [END functions_helloworld_error_2] +/* eslint-enable */ + +// [START functions_helloworld_error_3] +/** + * Background Cloud Function that throws an error. + * + * @param {object} event The Cloud Functions event. + * @param {function} The callback function. */ -exports.helloGCS = function helloGCS (context, data) { - console.log('Hello ' + (data.name || 'World') + '!'); - context.success(); +exports.helloError3 = function helloError3 (event, callback) { + // This will NOT be reported to Stackdriver errors + callback('I failed you'); }; -// [END helloGCS] +// [END functions_helloworld_error_3] diff --git a/functions/helloworld/package.json b/functions/helloworld/package.json index 4b538bb9a7..1878764919 100644 --- a/functions/helloworld/package.json +++ b/functions/helloworld/package.json @@ -6,9 +6,9 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- functions/helloworld/test/*.test.js" }, - "devDependencies": { - "mocha": "^2.5.3" + "dependencies": { + "@google/cloud-debug": "^0.9.0" } } diff --git a/functions/helloworld/test/index.test.js b/functions/helloworld/test/index.test.js index 039b0d1b2b..130d2f3dbe 100644 --- a/functions/helloworld/test/index.test.js +++ b/functions/helloworld/test/index.test.js @@ -1,141 +1,187 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var proxyquire = require('proxyquire').noCallThru(); -var helloworldSample = proxyquire('../', {}); - -function getMockContext () { - return { - success: sinon.stub(), - failure: sinon.stub() - }; -} - -describe('functions:helloworld', function () { - it('helloworld: should log a message', function () { - var expectedMsg = 'My Cloud Function: hi'; - var context = getMockContext(); - helloworldSample.helloWorld(context, { - message: 'hi' - }); +const proxyquire = require('proxyquire').noCallThru(); +const program = proxyquire(`../`, {}); + +describe(`functions:helloworld`, () => { + it(`helloworld: should log a message`, () => { + const expectedMsg = `My Cloud Function: hi`; + const callback = sinon.stub(); + + program.helloWorld({ + data: { + message: `hi` + } + }, callback); - assert.equal(context.success.calledOnce, true); - assert.equal(context.failure.called, false); - assert.equal(console.log.calledWith(expectedMsg), true); + assert.deepEqual(console.log.callCount, 1); + assert.deepEqual(console.log.firstCall.args, [expectedMsg]); + assert.deepEqual(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, []); }); - it('helloGET: should print hello world', function (done) { - var expectedMsg = 'Hello World!'; - helloworldSample.helloGET({}, { - send: function (message) { + it(`helloGET: should print hello world`, (done) => { + const expectedMsg = `Hello World!`; + + program.helloGET({}, { + send: (message) => { assert.equal(message, expectedMsg); done(); } }); }); - it('helloHttp: should print a name', function (done) { - var expectedMsg = 'Hello John!'; - helloworldSample.helloHttp({ + it(`helloHttp: should print a name`, (done) => { + const expectedMsg = `Hello John!`; + + program.helloHttp({ body: { - name: 'John' + name: `John` } }, { - send: function (message) { + send: (message) => { assert.equal(message, expectedMsg); done(); } }); }); - it('helloHttp: should print hello world', function (done) { - var expectedMsg = 'Hello World!'; - helloworldSample.helloHttp({ + it(`helloHttp: should print hello world`, (done) => { + const expectedMsg = `Hello World!`; + + program.helloHttp({ body: {} }, { - send: function (message) { + send: (message) => { assert.equal(message, expectedMsg); done(); } }); }); - it('helloBackground: should print a name', function () { - var expectedMsg = 'Hello John!'; - var context = getMockContext(); - helloworldSample.helloBackground(context, { - name: 'John' - }); + it(`helloBackground: should print a name`, () => { + const expectedMsg = `Hello John!`; + const callback = sinon.stub(); - assert.equal(context.success.calledOnce, true); - assert.equal(context.success.firstCall.args[0], expectedMsg); - assert.equal(context.failure.called, false); + program.helloBackground({ + data: { + name: `John` + } + }, callback); + + assert.deepEqual(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, [null, expectedMsg]); }); - it('helloBackground: should print hello world', function () { - var expectedMsg = 'Hello World!'; - var context = getMockContext(); - helloworldSample.helloBackground(context, {}); + it(`helloBackground: should print hello world`, () => { + const expectedMsg = `Hello World!`; + const callback = sinon.stub(); - assert.equal(context.success.calledOnce, true); - assert.equal(context.success.firstCall.args[0], expectedMsg); - assert.equal(context.failure.called, false); + program.helloBackground({ data: {} }, callback); + + assert.deepEqual(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, [null, expectedMsg]); }); - it('helloPubSub: should print a name', function () { - var expectedMsg = 'Hello Bob!'; - var context = getMockContext(); - helloworldSample.helloPubSub(context, { - name: 'Bob' - }); + it(`helloPubSub: should print a name`, () => { + const expectedMsg = `Hello Bob!`; + const callback = sinon.stub(); + + program.helloPubSub({ + data: { + data: new Buffer(`Bob`).toString(`base64`) + } + }, callback); + + assert.deepEqual(console.log.callCount, 1); + assert.deepEqual(console.log.firstCall.args, [expectedMsg]); + assert.deepEqual(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, []); + }); + + it(`helloPubSub: should print hello world`, () => { + const expectedMsg = `Hello World!`; + const callback = sinon.stub(); - assert.equal(context.success.calledOnce, true); - assert.equal(context.failure.called, false); - assert.equal(console.log.calledWith(expectedMsg), true); + program.helloPubSub({ data: {} }, callback); + + assert.deepEqual(console.log.callCount, 1); + assert.deepEqual(console.log.firstCall.args, [expectedMsg]); + assert.deepEqual(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, []); }); - it('helloPubSub: should print hello world', function () { - var expectedMsg = 'Hello World!'; - var context = getMockContext(); - helloworldSample.helloPubSub(context, {}); + it(`helloGCS: should print uploaded message`, () => { + const expectedMsg = `File foo uploaded.`; + const callback = sinon.stub(); + + program.helloGCS({ + data: { + name: `foo`, + resourceState: `exists` + } + }, callback); - assert.equal(context.success.calledOnce, true); - assert.equal(context.failure.called, false); - assert.equal(console.log.calledWith(expectedMsg), true); + assert.deepEqual(console.log.callCount, 1); + assert.deepEqual(console.log.firstCall.args, [expectedMsg]); + assert.deepEqual(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, []); }); - it('helloGCS: should print a name', function () { - var expectedMsg = 'Hello Sally!'; - var context = getMockContext(); - helloworldSample.helloGCS(context, { - name: 'Sally' - }); + it(`helloGCS: should print deleted message`, () => { + const expectedMsg = `File foo deleted.`; + const callback = sinon.stub(); + + program.helloGCS({ + data: { + name: `foo`, + resourceState: `not_exists` + } + }, callback); - assert.equal(context.success.calledOnce, true); - assert.equal(context.failure.called, false); - assert.equal(console.log.calledWith(expectedMsg), true); + assert.deepEqual(console.log.callCount, 1); + assert.deepEqual(console.log.firstCall.args, [expectedMsg]); + assert.deepEqual(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, []); }); - it('helloGCS: should print hello world', function () { - var expectedMsg = 'Hello World!'; - var context = getMockContext(); - helloworldSample.helloGCS(context, {}); + it(`helloError: should throw an error`, () => { + const expectedMsg = `I failed you`; + + assert.throws(() => { + program.helloError(); + }, Error, expectedMsg); + }); + + it(`helloError2: should throw a value`, () => { + assert.throws(() => { + program.helloError2(); + }); + }); + + it(`helloError3: callback shoud return an errback value`, () => { + const expectedMsg = `I failed you`; + const callback = sinon.stub(); + + program.helloError3({}, callback); - assert.equal(context.success.calledOnce, true); - assert.equal(context.failure.called, false); - assert.equal(console.log.calledWith(expectedMsg), true); + assert.deepEqual(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, [expectedMsg]); }); }); diff --git a/functions/http/index.js b/functions/http/index.js index af03b66583..1c044aef8e 100644 --- a/functions/http/index.js +++ b/functions/http/index.js @@ -1,19 +1,21 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -// [START helloworld] +// [START functions_http_helloworld] /** * Responds to any HTTP request that can provide a "message" field in the body. * @@ -30,9 +32,45 @@ exports.helloWorld = function helloWorld (req, res) { res.status(200).end(); } }; -// [END helloworld] +// [END functions_http_helloworld] + +// [START functions_http_content] +/** + * Responds to any HTTP request that can provide a "message" field in the body. + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.helloContent = function helloContent (req, res) { + let name; + + switch (req.get('content-type')) { + // '{"name":"John"}' + case 'application/json': + name = req.body.name; + break; + + // 'John', stored in a Buffer + case 'application/octet-stream': + name = req.body.toString(); // Convert buffer to a string + break; + + // 'John' + case 'text/plain': + name = req.body; + break; + + // 'name=John' + case 'application/x-www-form-urlencoded': + name = req.body.name; + break; + } + + res.status(200).send(`Hello ${name || 'World'}!`); +}; +// [END functions_http_content] -// [START helloHttp] +// [START functions_http_method] function handleGET (req, res) { // Do something with the GET request res.status(200).send('Hello World!'); @@ -65,40 +103,4 @@ exports.helloHttp = function helloHttp (req, res) { break; } }; -// [END helloHttp] - -// [START helloContent] -/** - * Responds to any HTTP request that can provide a "message" field in the body. - * - * @param {Object} req Cloud Function request context. - * @param {Object} res Cloud Function response context. - */ -exports.helloContent = function helloContent (req, res) { - var name; - - switch (req.get('content-type')) { - // '{"name":"John"}' - case 'application/json': - name = req.body.name; - break; - - // 'John', stored in a Buffer - case 'application/octet-stream': - name = req.body.toString(); // Convert buffer to a string - break; - - // 'John' - case 'text/plain': - name = req.body; - break; - - // 'name=John' - case 'application/x-www-form-urlencoded': - name = req.body.name; - break; - } - - res.status(200).send('Hello ' + (name || 'World') + '!'); -}; -// [END helloContent] +// [END functions_http_method] diff --git a/functions/http/package.json b/functions/http/package.json index dd0e81a2d8..22e4204dbf 100644 --- a/functions/http/package.json +++ b/functions/http/package.json @@ -6,9 +6,6 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" - }, - "devDependencies": { - "mocha": "^2.5.3" + "test": "cd ..; npm run t -- functions/http/test/*.test.js" } } diff --git a/functions/http/test/index.test.js b/functions/http/test/index.test.js index 58fa52a273..59ba79ae1b 100644 --- a/functions/http/test/index.test.js +++ b/functions/http/test/index.test.js @@ -1,26 +1,27 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var proxyquire = require('proxyquire').noCallThru(); +const proxyquire = require('proxyquire').noCallThru(); function getSample () { - var requestPromise = sinon.stub().returns(new Promise(function (resolve) { - resolve('test'); - })); + const requestPromise = sinon.stub().returns(new Promise((resolve) => resolve(`test`))); + return { - sample: proxyquire('../', { + sample: proxyquire(`../`, { 'request-promise': requestPromise }), mocks: { @@ -30,13 +31,14 @@ function getSample () { } function getMocks () { - var req = { + const req = { headers: {}, get: function (header) { return this.headers[header]; } }; - sinon.spy(req, 'get'); + sinon.spy(req, `get`); + return { req: req, res: { @@ -48,130 +50,130 @@ function getMocks () { }; } -describe('functions:http', function () { - it('http:helloworld: should error with no message', function () { - var mocks = getMocks(); - var httpSample = getSample(); +describe(`functions:http`, () => { + it(`http:helloworld: should error with no message`, () => { + const mocks = getMocks(); + const httpSample = getSample(); mocks.req.body = {}; httpSample.sample.helloWorld(mocks.req, mocks.res); assert.equal(mocks.res.status.calledOnce, true); assert.equal(mocks.res.status.firstCall.args[0], 400); assert.equal(mocks.res.send.calledOnce, true); - assert.equal(mocks.res.send.firstCall.args[0], 'No message defined!'); + assert.equal(mocks.res.send.firstCall.args[0], `No message defined!`); }); - it('http:helloworld: should log message', function () { - var mocks = getMocks(); - var httpSample = getSample(); + it(`http:helloworld: should log message`, () => { + const mocks = getMocks(); + const httpSample = getSample(); mocks.req.body = { - message: 'hi' + message: `hi` }; httpSample.sample.helloWorld(mocks.req, mocks.res); assert.equal(mocks.res.status.calledOnce, true); assert.equal(mocks.res.status.firstCall.args[0], 200); assert.equal(mocks.res.end.calledOnce, true); - assert.equal(console.log.calledWith('hi'), true); + assert.equal(console.log.calledWith(`hi`), true); }); - it('http:helloHttp: should handle GET', function () { - var mocks = getMocks(); - var httpSample = getSample(); - mocks.req.method = 'GET'; + it(`http:helloHttp: should handle GET`, () => { + const mocks = getMocks(); + const httpSample = getSample(); + mocks.req.method = `GET`; httpSample.sample.helloHttp(mocks.req, mocks.res); assert.equal(mocks.res.status.calledOnce, true); assert.equal(mocks.res.status.firstCall.args[0], 200); assert.equal(mocks.res.send.calledOnce, true); - assert.equal(mocks.res.send.firstCall.args[0], 'Hello World!'); + assert.equal(mocks.res.send.firstCall.args[0], `Hello World!`); }); - it('http:helloHttp: should handle PUT', function () { - var mocks = getMocks(); - var httpSample = getSample(); - mocks.req.method = 'PUT'; + it(`http:helloHttp: should handle PUT`, () => { + const mocks = getMocks(); + const httpSample = getSample(); + mocks.req.method = `PUT`; httpSample.sample.helloHttp(mocks.req, mocks.res); assert.equal(mocks.res.status.calledOnce, true); assert.equal(mocks.res.status.firstCall.args[0], 403); assert.equal(mocks.res.send.calledOnce, true); - assert.equal(mocks.res.send.firstCall.args[0], 'Forbidden!'); + assert.equal(mocks.res.send.firstCall.args[0], `Forbidden!`); }); - it('http:helloHttp: should handle other methods', function () { - var mocks = getMocks(); - var httpSample = getSample(); - mocks.req.method = 'POST'; + it(`http:helloHttp: should handle other methods`, () => { + const mocks = getMocks(); + const httpSample = getSample(); + mocks.req.method = `POST`; httpSample.sample.helloHttp(mocks.req, mocks.res); assert.equal(mocks.res.status.calledOnce, true); assert.equal(mocks.res.status.firstCall.args[0], 500); assert.equal(mocks.res.send.calledOnce, true); - assert.deepEqual(mocks.res.send.firstCall.args[0], { error: 'Something blew up!' }); + assert.deepEqual(mocks.res.send.firstCall.args[0], { error: `Something blew up!` }); }); - it('http:helloContent: should handle application/json', function () { - var mocks = getMocks(); - var httpSample = getSample(); - mocks.req.headers['content-type'] = 'application/json'; - mocks.req.body = { name: 'John' }; + it(`http:helloContent: should handle application/json`, () => { + const mocks = getMocks(); + const httpSample = getSample(); + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = { name: `John` }; httpSample.sample.helloContent(mocks.req, mocks.res); assert.equal(mocks.res.status.calledOnce, true); assert.equal(mocks.res.status.firstCall.args[0], 200); assert.equal(mocks.res.send.calledOnce, true); - assert.deepEqual(mocks.res.send.firstCall.args[0], 'Hello John!'); + assert.deepEqual(mocks.res.send.firstCall.args[0], `Hello John!`); }); - it('http:helloContent: should handle application/octet-stream', function () { - var mocks = getMocks(); - var httpSample = getSample(); - mocks.req.headers['content-type'] = 'application/octet-stream'; - mocks.req.body = new Buffer('John'); + it(`http:helloContent: should handle application/octet-stream`, () => { + const mocks = getMocks(); + const httpSample = getSample(); + mocks.req.headers[`content-type`] = `application/octet-stream`; + mocks.req.body = new Buffer(`John`); httpSample.sample.helloContent(mocks.req, mocks.res); assert.equal(mocks.res.status.calledOnce, true); assert.equal(mocks.res.status.firstCall.args[0], 200); assert.equal(mocks.res.send.calledOnce, true); - assert.deepEqual(mocks.res.send.firstCall.args[0], 'Hello John!'); + assert.deepEqual(mocks.res.send.firstCall.args[0], `Hello John!`); }); - it('http:helloContent: should handle text/plain', function () { - var mocks = getMocks(); - var httpSample = getSample(); - mocks.req.headers['content-type'] = 'text/plain'; - mocks.req.body = 'John'; + it(`http:helloContent: should handle text/plain`, () => { + const mocks = getMocks(); + const httpSample = getSample(); + mocks.req.headers[`content-type`] = `text/plain`; + mocks.req.body = `John`; httpSample.sample.helloContent(mocks.req, mocks.res); assert.equal(mocks.res.status.calledOnce, true); assert.equal(mocks.res.status.firstCall.args[0], 200); assert.equal(mocks.res.send.calledOnce, true); - assert.deepEqual(mocks.res.send.firstCall.args[0], 'Hello John!'); + assert.deepEqual(mocks.res.send.firstCall.args[0], `Hello John!`); }); - it('http:helloContent: should handle application/x-www-form-urlencoded', function () { - var mocks = getMocks(); - var httpSample = getSample(); - mocks.req.headers['content-type'] = 'application/x-www-form-urlencoded'; - mocks.req.body = { name: 'John' }; + it(`http:helloContent: should handle application/x-www-form-urlencoded`, () => { + const mocks = getMocks(); + const httpSample = getSample(); + mocks.req.headers[`content-type`] = `application/x-www-form-urlencoded`; + mocks.req.body = { name: `John` }; httpSample.sample.helloContent(mocks.req, mocks.res); assert.equal(mocks.res.status.calledOnce, true); assert.equal(mocks.res.status.firstCall.args[0], 200); assert.equal(mocks.res.send.calledOnce, true); - assert.deepEqual(mocks.res.send.firstCall.args[0], 'Hello John!'); + assert.deepEqual(mocks.res.send.firstCall.args[0], `Hello John!`); }); - it('http:helloContent: should handle other', function () { - var mocks = getMocks(); - var httpSample = getSample(); + it(`http:helloContent: should handle other`, () => { + const mocks = getMocks(); + const httpSample = getSample(); httpSample.sample.helloContent(mocks.req, mocks.res); assert.equal(mocks.res.status.calledOnce, true); assert.equal(mocks.res.status.firstCall.args[0], 200); assert.equal(mocks.res.send.calledOnce, true); - assert.deepEqual(mocks.res.send.firstCall.args[0], 'Hello World!'); + assert.deepEqual(mocks.res.send.firstCall.args[0], `Hello World!`); }); }); diff --git a/functions/log/index.js b/functions/log/index.js index a51125817e..7470ca23b0 100644 --- a/functions/log/index.js +++ b/functions/log/index.js @@ -1,97 +1,97 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -// [START log] -exports.helloWorld = function helloWorld (context, data) { +// [START functions_log_helloworld] +exports.helloWorld = function helloWorld (event, callback) { console.log('I am a log entry!'); - context.success(); + callback(); }; -// [END log] +// [END functions_log_helloworld] -exports.retrieve = function retrieve () { - // [START retrieve] - // By default, the client will authenticate using the service account file - // specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable and use - // the project specified by the GCLOUD_PROJECT environment variable. See - // https://googlecloudplatform.github.io/gcloud-node/#/docs/google-cloud/latest/guides/authentication - var Logging = require('@google-cloud/logging'); +// [START functions_log_retrieve] +// By default, the client will authenticate using the service account file +// specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable and use +// the project specified by the GCLOUD_PROJECT environment variable. See +// https://googlecloudplatform.github.io/gcloud-node/#/docs/google-cloud/latest/guides/authentication +const Logging = require('@google-cloud/logging'); - // Instantiate a logging client - var logging = Logging(); +function getLogEntries () { + // Instantiates a client + const logging = Logging(); - // Retrieve the latest Cloud Function log entries - // See https://googlecloudplatform.github.io/gcloud-node/#/docs/logging - logging.getEntries({ + const options = { pageSize: 10, filter: 'resource.type="cloud_function"' - }, function (err, entries) { - if (err) { - console.error(err); - } else { - console.log(entries); - } - }); - // [END retrieve] -}; + }; -exports.getMetrics = function getMetrics () { - // [START getMetrics] - var google = require('googleapis'); - var monitoring = google.monitoring('v3'); + // Retrieve the latest Cloud Function log entries + // See https://googlecloudplatform.github.io/gcloud-node/#/docs/logging + return logging.getEntries(options) + .then(([entries]) => { + console.log('Entries:'); + entries.forEach((entry) => console.log(entry)); + return entries; + }); +} +// [END functions_log_retrieve] - google.auth.getApplicationDefault(function (err, authClient) { - if (err) { - return console.error('Authentication failed', err); - } - if (authClient.createScopedRequired && authClient.createScopedRequired()) { - var scopes = [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/monitoring', - 'https://www.googleapis.com/auth/monitoring.read', - 'https://www.googleapis.com/auth/monitoring.write' - ]; - authClient = authClient.createScoped(scopes); - } +// [START functions_log_get_metrics] +// By default, the client will authenticate using the service account file +// specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable and use +// the project specified by the GCLOUD_PROJECT environment variable. See +// https://googlecloudplatform.github.io/gcloud-node/#/docs/google-cloud/latest/guides/authentication +const Monitoring = require('@google-cloud/monitoring'); - // Format a date according to RFC33339 with milliseconds format - function formatDate (date) { - return JSON.parse(JSON.stringify(date).replace('Z', '000Z')); - } +function getMetrics (callback) { + // Instantiates a client + const monitoring = Monitoring.v3().metricServiceApi(); - // Create two datestrings, a start and end range - var oneWeekAgo = new Date(); - var now = new Date(); - oneWeekAgo.setHours(oneWeekAgo.getHours() - (7 * 24)); - oneWeekAgo = formatDate(oneWeekAgo); - now = formatDate(now); + // Create two datestrings, a start and end range + let oneWeekAgo = new Date(); + oneWeekAgo.setHours(oneWeekAgo.getHours() - (7 * 24)); - monitoring.projects.timeSeries.list({ - auth: authClient, - // There is also cloudfunctions.googleapis.com/function/execution_count - filter: 'metric.type="cloudfunctions.googleapis.com/function/execution_times"', - pageSize: 10, - 'interval.startTime': oneWeekAgo, - 'interval.endTime': now, - name: 'projects/' + process.env.GCLOUD_PROJECT - }, function (err, results) { - if (err) { - console.error(err); - } else { - console.log(results.timeSeries); + const options = { + name: monitoring.projectPath(process.env.GCLOUD_PROJECT), + // There is also: cloudfunctions.googleapis.com/function/execution_count + filter: 'metric.type="cloudfunctions.googleapis.com/function/execution_times"', + interval: { + startTime: { + seconds: oneWeekAgo.getTime() / 1000 + }, + endTime: { + seconds: Date.now() / 1000 } - }); - }); - // [END getMetrics] -}; + }, + view: 1 + }; + + console.log('Data:'); + + let error; + + // Iterate over all elements. + monitoring.listTimeSeries(options) + .on('error', (err) => { + error = err; + }) + .on('data', (element) => console.log(element)) + .on('end', () => callback(error)); + // [END functions_log_get_metrics] +} + +exports.getLogEntries = getLogEntries; +exports.getMetrics = getMetrics; diff --git a/functions/log/package.json b/functions/log/package.json index a90bf288e3..22f72e1651 100644 --- a/functions/log/package.json +++ b/functions/log/package.json @@ -6,13 +6,10 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- functions/log/test/*.test.js" }, "dependencies": { - "@google-cloud/logging": "^0.1.1", - "googleapis": "^12.2.0" - }, - "devDependencies": { - "mocha": "^3.0.2" + "@google-cloud/logging": "^0.5.0", + "@google-cloud/monitoring": "^0.1.0" } } diff --git a/functions/log/test/index.test.js b/functions/log/test/index.test.js index cd6ff98a80..9e77e5e3f9 100644 --- a/functions/log/test/index.test.js +++ b/functions/log/test/index.test.js @@ -1,117 +1,84 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var proxyquire = require('proxyquire').noCallThru(); - -var authClient = {}; +const proxyquire = require(`proxyquire`).noCallThru(); function getSample () { - var auth = { - getApplicationDefault: sinon.stub().callsArgWith(0, null, authClient) + const results = [[{}], {}]; + const stream = { + on: sinon.stub().returnsThis() }; - var monitoring = { - projects: { - timeSeries: { - list: sinon.stub().callsArgWith(1, null, { - timeSeries: 'series' - }) - } - } + stream.on.withArgs('end').yields(); + + const monitoring = { + projectPath: sinon.stub(), + listTimeSeries: sinon.stub().returns(stream) }; - var logging = { - getEntries: sinon.stub().callsArgWith(1, null, 'entries') + const logging = { + getEntries: sinon.stub().returns(Promise.resolve(results)) }; + return { - sample: proxyquire('../', { - googleapis: { - auth: auth, - monitoring: sinon.stub().returns(monitoring) - }, - '@google-cloud/logging': sinon.stub().returns(logging) + program: proxyquire(`../`, { + '@google-cloud/logging': sinon.stub().returns(logging), + '@google-cloud/monitoring': { + v3: sinon.stub().returns({ + metricServiceApi: sinon.stub().returns(monitoring) + }) + } }), mocks: { - auth: auth, monitoring: monitoring, - logging: logging + logging: logging, + results: results } }; } -describe('functions:log', function () { - it('should write to log', function () { - var expectedMsg = 'I am a log entry!'; - getSample().sample.helloWorld({ - success: function (result) { - assert.equal(result, undefined); - assert.equal(console.log.called, true); - assert.equal(console.log.calledWith(expectedMsg), true); - }, - failure: assert.fail - }); - }); +describe(`functions:log`, () => { + it(`should write to log`, () => { + const expectedMsg = `I am a log entry!`; + const callback = sinon.stub(); - it('retrieve: should retrieve logs', function () { - var logSample = getSample(); - logSample.sample.retrieve(); - assert.equal(console.log.calledWith('entries'), true); - }); + getSample().program.helloWorld({}, callback); - it('retrieve: handles error', function () { - var expectedMsg = 'entries error'; - var logSample = getSample(); - logSample.mocks.logging.getEntries = sinon.stub().callsArgWith(1, expectedMsg); - logSample.sample.retrieve(); - assert.equal(console.error.calledWith(expectedMsg), true); + assert.equal(console.log.callCount, 1); + assert.deepEqual(console.log.firstCall.args, [expectedMsg]); + assert.equal(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, []); }); - it('getMetrics: should retrieve metrics', function () { - var logSample = getSample(); - logSample.sample.getMetrics(); - assert.equal(console.log.calledWith('series'), true); - }); + it(`getLogEntries: should retrieve logs`, () => { + const sample = getSample(); - it('getMetrics: creates with scope', function () { - var authClient = { - createScopedRequired: sinon.stub().returns(true), - createScoped: sinon.stub().returns('foo') - }; - var logSample = getSample(); - logSample.mocks.auth.getApplicationDefault = sinon.stub().callsArgWith(0, null, authClient); - logSample.sample.getMetrics(); - assert.deepEqual(authClient.createScoped.firstCall.args[0], [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/monitoring', - 'https://www.googleapis.com/auth/monitoring.read', - 'https://www.googleapis.com/auth/monitoring.write' - ]); + return sample.program.getLogEntries() + .then((entries) => { + assert.equal(console.log.calledWith(`Entries:`), true); + assert.strictEqual(entries, sample.mocks.results[0]); + }); }); - it('getMetrics: handles auth error', function () { - var expectedMsg = 'auth error'; - var logSample = getSample(); - logSample.mocks.auth.getApplicationDefault = sinon.stub().callsArgWith(0, expectedMsg); - logSample.sample.getMetrics(); - assert.equal(console.error.calledWith('Authentication failed', expectedMsg), true); - }); + it(`getMetrics: should retrieve metrics`, () => { + const sample = getSample(); + const callback = sinon.stub(); + + sample.program.getMetrics(callback); - it('getMetrics: handles time series error', function () { - var expectedMsg = 'time series error'; - var logSample = getSample(); - logSample.mocks.monitoring.projects.timeSeries.list = sinon.stub().callsArgWith(1, expectedMsg); - logSample.sample.getMetrics(); - assert.equal(console.error.calledWith(expectedMsg), true); + assert.equal(callback.callCount, 1); }); }); diff --git a/functions/ocr/app/config.default.json b/functions/ocr/app/config.default.json index 9766ceb121..80a2589c1a 100644 --- a/functions/ocr/app/config.default.json +++ b/functions/ocr/app/config.default.json @@ -1,5 +1,4 @@ { - "TRANSLATE_API_KEY": "[YOUR_API_KEY]", "RESULT_TOPIC": "[RESULT_TOPIC_NAME]", "RESULT_BUCKET": "[RESULT_BUCKET_NAME]", "TRANSLATE_TOPIC": "[TRANSLATE_TOPIC_NAME]", diff --git a/functions/ocr/app/index.js b/functions/ocr/app/index.js index 6d278cecf4..6489d79218 100644 --- a/functions/ocr/app/index.js +++ b/functions/ocr/app/index.js @@ -1,105 +1,95 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -// [START ocr_setup] -var async = require('async'); -var config = require('./config.json'); +// [START functions_ocr_setup] +const config = require('./config.json'); // Get a reference to the Pub/Sub component -var pubsub = require('@google-cloud/pubsub')(); +const pubsub = require('@google-cloud/pubsub')(); // Get a reference to the Cloud Storage component -var storage = require('@google-cloud/storage')(); +const storage = require('@google-cloud/storage')(); // Get a reference to the Cloud Vision API component -var vision = require('@google-cloud/vision')(); +const vision = require('@google-cloud/vision')(); // Get a reference to the Translate API component -var translate = require('@google-cloud/translate')({ - key: config.TRANSLATE_API_KEY -}); -// [END ocr_setup] +const translate = require('@google-cloud/translate')(); +// [END functions_ocr_setup] -// [START ocr_publish] +// [START functions_ocr_publish] /** * Publishes the result to the given pubsub topic and returns a Promise. * * @param {string} topicName Name of the topic on which to publish. - * @param {Object} data The data to publish. - * @param {Function} callback Callback function. + * @param {object} data The message data to publish. */ -function publishResult (topicName, data, callback) { - return pubsub.topic(topicName).get({ - autoCreate: true - }, function (err, topic) { - if (err) { - return callback(err); - } - // Pub/Sub messages must be valid JSON objects with a data property. - return topic.publish({ - data: data - }, callback); - }); +function publishResult (topicName, data) { + return pubsub.topic(topicName).get({ autoCreate: true }) + .then(([topic]) => topic.publish(data)); } -// [END ocr_publish] +// [END functions_ocr_publish] -// [START ocr_detect] +// [START functions_ocr_detect] /** * Detects the text in an image using the Google Vision API. * - * @param {string} filename Name of the file to scan. - * @param {Object} image Cloud Storage File instance. + * @param {object} file Cloud Storage File instance. + * @returns {Promise} */ -function detectText (filename, image, callback) { - var text; +function detectText (file) { + let text; + + console.log(`Looking for text in image ${file.name}`); + return vision.detectText(file) + .then(([_text]) => { + if (Array.isArray(_text)) { + text = _text[0]; + } else { + text = _text; + } + console.log(`Extracted text from image (${text.length} chars)`); + return translate.detect(text); + }) + .then(([detection]) => { + if (Array.isArray(detection)) { + detection = detection[0]; + } + console.log(`Detected language "${detection.language}" for ${file.name}`); - return async.waterfall([ - // Read the text from the image. - function (cb) { - console.log('Looking for text in file ' + filename); - vision.detectText(image, cb); - }, - // Detect the language to avoid unnecessary translations - function (result, apiResponse, cb) { - text = result[0]; - console.log('Extracted text from image (' + text.length + ' chars)'); - translate.detect(text, cb); - }, - // Publish results - function (result, cb) { - console.log('Detected language "' + result.language + '" for ' + filename); // Submit a message to the bus for each language we're going to translate to - var tasks = config.TO_LANG.map(function (lang) { - var topicName = config.TRANSLATE_TOPIC; - if (result.language === lang) { + const tasks = config.TO_LANG.map((lang) => { + let topicName = config.TRANSLATE_TOPIC; + if (detection.language === lang) { topicName = config.RESULT_TOPIC; } - var payload = { + const messageData = { text: text, - filename: filename, + filename: file.name, lang: lang, - from: result.language - }; - return function (cb) { - publishResult(topicName, payload, cb); + from: detection.language }; + + return publishResult(topicName, messageData); }); - async.parallel(tasks, cb); - } - ], callback); + + return Promise.all(tasks); + }); } -// [END ocr_detect] +// [END functions_ocr_detect] -// [START ocr_rename] +// [START functions_ocr_rename] /** * Appends a .txt suffix to the image name. * @@ -108,168 +98,133 @@ function detectText (filename, image, callback) { * @returns {string} The new filename. */ function renameImageForSave (filename, lang) { - var dotIndex = filename.indexOf('.'); - var suffix = '_to_' + lang + '.txt'; - if (dotIndex !== -1) { - filename = filename.replace(/\.[^/.]+$/, suffix); - } else { - filename += suffix; - } - return filename; + return `${filename}_to_${lang}.txt`; } -// [END ocr_rename] +// [END functions_ocr_rename] -// [START ocr_process] +// [START functions_ocr_process] /** * Cloud Function triggered by Cloud Storage when a file is uploaded. * - * @param {Object} context Cloud Function context. - * @param {Function} context.success Success callback. - * @param {Function} context.failure Failure callback. - * @param {Object} data Request data, in this case an object provided by Cloud Storage. - * @param {string} data.bucket Name of the Cloud Storage bucket. - * @param {string} data.name Name of the file. - * @param {string} [data.timeDeleted] Time the file was deleted if this is a deletion event. - * @see https://cloud.google.com/storage/docs/json_api/v1/objects#resource + * @param {object} event The Cloud Functions event. + * @param {object} event.data A Google Cloud Storage File object. */ -exports.processImage = function processImage (context, data) { - try { - if (data.hasOwnProperty('timeDeleted')) { - // This was a deletion event, we don't want to process this - return context.done(); - } - - if (!data.bucket) { - throw new Error('Bucket not provided. Make sure you have a ' + - '"bucket" property in your request'); - } - if (!data.name) { - throw new Error('Filename not provided. Make sure you have a ' + - '"name" property in your request'); - } +exports.processImage = function processImage (event) { + let file = event.data; + + return Promise.resolve() + .then(() => { + if (file.resourceState === 'not_exists') { + // This was a deletion event, we don't want to process this + return; + } - var bucket = storage.bucket(data.bucket); - var file = bucket.file(data.name); - detectText(data.name, file, function (err) { - if (err) { - console.error(err); - return context.failure(err); + if (!file.bucket) { + throw new Error('Bucket not provided. Make sure you have a "bucket" property in your request'); + } + if (!file.name) { + throw new Error('Filename not provided. Make sure you have a "name" property in your request'); } - console.log('Processed ' + data.name); - return context.success(); + + file = storage.bucket(file.bucket).file(file.name); + + return detectText(file); + }) + .then(() => { + console.log(`File ${file.name} processed.`); }); - } catch (err) { - console.error(err); - return context.failure(err.message); - } }; -// [END ocr_process] +// [END functions_ocr_process] -// [START ocr_translate] +// [START functions_ocr_translate] /** * Translates text using the Google Translate API. Triggered from a message on * a Pub/Sub topic. * - * @param {Object} context Cloud Function context. - * @param {Function} context.success Success callback. - * @param {Function} context.failure Failure callback. - * @param {Object} data Request data, in this case an object provided by the Pub/Sub trigger. - * @param {Object} data.text Text to be translated. - * @param {Object} data.filename Name of the filename that contained the text. - * @param {Object} data.lang Language to translate to. + * @param {object} event The Cloud Functions event. + * @param {object} event.data The Cloud Pub/Sub Message object. + * @param {string} event.data.data The "data" property of the Cloud Pub/Sub + * Message. This property will be a base64-encoded string that you must decode. */ -exports.translateText = function translateText (context, data) { - try { - if (!data.text) { - throw new Error('Text not provided. Make sure you have a ' + - '"text" property in your request'); - } - if (!data.filename) { - throw new Error('Filename not provided. Make sure you have a ' + - '"filename" property in your request'); - } - if (!data.lang) { - throw new Error('Language not provided. Make sure you have a ' + - '"lang" property in your request'); - } - - console.log('Translating text into ' + data.lang); - return translate.translate(data.text, { - from: data.from, - to: data.lang - }, function (err, translation) { - if (err) { - console.error(err); - return context.failure(err); +exports.translateText = function translateText (event) { + const pubsubMessage = event.data; + const jsonStr = Buffer.from(pubsubMessage.data, 'base64').toString(); + const payload = JSON.parse(jsonStr); + + return Promise.resolve() + .then(() => { + if (!payload.text) { + throw new Error('Text not provided. Make sure you have a "text" property in your request'); } + if (!payload.filename) { + throw new Error('Filename not provided. Make sure you have a "filename" property in your request'); + } + if (!payload.lang) { + throw new Error('Language not provided. Make sure you have a "lang" property in your request'); + } + + const options = { + from: payload.from, + to: payload.lang + }; - return publishResult(config.RESULT_TOPIC, { + console.log(`Translating text into ${payload.lang}`); + return translate.translate(payload.text, options); + }) + .then(([translation]) => { + const messageData = { text: translation, - filename: data.filename, - lang: data.lang - }, function (err) { - if (err) { - console.error(err); - return context.failure(err); - } - console.log('Text translated to ' + data.lang); - return context.success(); - }); + filename: payload.filename, + lang: payload.lang + }; + + return publishResult(config.RESULT_TOPIC, messageData); + }) + .then(() => { + console.log(`Text translated to ${payload.lang}`); }); - } catch (err) { - console.error(err); - return context.failure(err.message); - } }; -// [END ocr_translate] +// [END functions_ocr_translate] -// [START ocr_save] +// [START functions_ocr_save] /** * Saves the data packet to a file in GCS. Triggered from a message on a Pub/Sub * topic. * - * @param {Object} context Cloud Function context. - * @param {Function} context.success Success callback. - * @param {Function} context.failure Failure callback. - * @param {Object} data Request data, in this case an object provided by the Pub/Sub trigger. - * @param {Object} data.text Text to save. - * @param {Object} data.filename Name of the filename that contained the text. - * @param {Object} data.lang Language of the text. + * @param {object} event The Cloud Functions event. + * @param {object} event.data The Cloud Pub/Sub Message object. + * @param {string} event.data.data The "data" property of the Cloud Pub/Sub + * Message. This property will be a base64-encoded string that you must decode. */ -exports.saveResult = function saveResult (context, data) { - try { - if (!data.text) { - throw new Error('Text not provided. Make sure you have a ' + - '"text" property in your request'); - } - if (!data.filename) { - throw new Error('Filename not provided. Make sure you have a ' + - '"filename" property in your request'); - } - if (!data.lang) { - throw new Error('Language not provided. Make sure you have a ' + - '"lang" property in your request'); - } +exports.saveResult = function saveResult (event) { + const pubsubMessage = event.data; + const jsonStr = Buffer.from(pubsubMessage.data, 'base64').toString(); + const payload = JSON.parse(jsonStr); + + return Promise.resolve() + .then(() => { + if (!payload.text) { + throw new Error('Text not provided. Make sure you have a "text" property in your request'); + } + if (!payload.filename) { + throw new Error('Filename not provided. Make sure you have a "filename" property in your request'); + } + if (!payload.lang) { + throw new Error('Language not provided. Make sure you have a "lang" property in your request'); + } - console.log('Received request to save file ' + data.filename); + console.log(`Received request to save file ${payload.filename}`); - var bucketName = config.RESULT_BUCKET; - var filename = renameImageForSave(data.filename, data.lang); - var file = storage.bucket(bucketName).file(filename); + const bucketName = config.RESULT_BUCKET; + const filename = renameImageForSave(payload.filename, payload.lang); + const file = storage.bucket(bucketName).file(filename); - console.log('Saving result to ' + filename + ' in bucket ' + bucketName); + console.log(`Saving result to ${filename} in bucket ${bucketName}`); - file.save(data.text, function (err) { - if (err) { - console.error(err); - return context.failure(err); - } - console.log('Text written to ' + filename); - return context.success(); + return file.save(payload.text); + }) + .then(() => { + console.log(`File saved.`); }); - } catch (err) { - console.error(err); - return context.failure(err.message); - } }; -// [END ocr_save] +// [END functions_ocr_save] diff --git a/functions/ocr/app/package.json b/functions/ocr/app/package.json index 4592910428..3ffc5b08e2 100644 --- a/functions/ocr/app/package.json +++ b/functions/ocr/app/package.json @@ -6,16 +6,12 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- functions/ocr/app/test/*.test.js" }, "dependencies": { - "@google-cloud/pubsub": "^0.2.0", - "@google-cloud/storage": "^0.1.1", - "@google-cloud/translate": "^0.2.0", - "@google-cloud/vision": "^0.2.0", - "async": "^2.0.1" - }, - "devDependencies": { - "mocha": "^3.0.2" + "@google-cloud/pubsub": "^0.5.0", + "@google-cloud/storage": "^0.4.0", + "@google-cloud/translate": "^0.5.0", + "@google-cloud/vision": "^0.5.0" } } diff --git a/functions/ocr/app/test/index.test.js b/functions/ocr/app/test/index.test.js index 776183bc0e..32e0ff2123 100644 --- a/functions/ocr/app/test/index.test.js +++ b/functions/ocr/app/test/index.test.js @@ -1,65 +1,68 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var proxyquire = require('proxyquire').noCallThru(); +const proxyquire = require(`proxyquire`).noCallThru(); -var bucket = 'bucket'; -var name = 'name'; -var text = 'text'; -var filename = 'filename'; -var lang = 'lang'; -var translation = 'translation'; +const bucketName = `my-bucket`; +const filename = `image.jpg`; +const text = `text`; +const lang = `lang`; +const translation = `translation`; function getSample () { - var config = { - TRANSLATE_API_KEY: 'key', - RESULT_TOPIC: 'result-topic', - RESULT_BUCKET: 'result-bucket', - TRANSLATE_TOPIC: 'translate-topic', + const config = { + RESULT_TOPIC: `result-topic`, + RESULT_BUCKET: `result-bucket`, + TRANSLATE_TOPIC: `translate-topic`, TRANSLATE: true, - TO_LANG: ['en', 'fr', 'es', 'ja', 'ru'] + TO_LANG: [`en`, `fr`, `es`, `ja`, `ru`] }; - var topic = { - publish: sinon.stub().callsArg(1) + const topic = { + publish: sinon.stub().returns(Promise.resolve([])) }; - topic.get = sinon.stub().callsArgWith(1, null, topic); - var file = { - save: sinon.stub().callsArg(1) + topic.get = sinon.stub().returns(Promise.resolve([topic])); + const file = { + save: sinon.stub().returns(Promise.resolve([])), + bucket: bucketName, + name: filename }; - var bucket = { + const bucket = { file: sinon.stub().returns(file) }; - var pubsubMock = { + const pubsubMock = { topic: sinon.stub().returns(topic) }; - var storageMock = { + const storageMock = { bucket: sinon.stub().returns(bucket) }; - var visionMock = { - detectText: sinon.stub().callsArg(1) + const visionMock = { + detectText: sinon.stub().returns(Promise.resolve([ text ])) }; - var translateMock = { - detect: sinon.stub().callsArg(1) + const translateMock = { + detect: sinon.stub().returns(Promise.resolve([{ language: `ja` }])), + translate: sinon.stub().returns(Promise.resolve([translation])) }; - var PubsubMock = sinon.stub().returns(pubsubMock); - var StorageMock = sinon.stub().returns(storageMock); - var VisionMock = sinon.stub().returns(visionMock); - var TranslateMock = sinon.stub().returns(translateMock); + const PubsubMock = sinon.stub().returns(pubsubMock); + const StorageMock = sinon.stub().returns(storageMock); + const VisionMock = sinon.stub().returns(visionMock); + const TranslateMock = sinon.stub().returns(translateMock); return { - sample: proxyquire('../', { + program: proxyquire(`../`, { '@google-cloud/translate': TranslateMock, '@google-cloud/vision': VisionMock, '@google-cloud/pubsub': PubsubMock, @@ -67,372 +70,200 @@ function getSample () { './config.json': config }), mocks: { + config, pubsub: pubsubMock, storage: storageMock, bucket: bucket, - file: file, + file, vision: visionMock, translate: translateMock, - topic: topic + topic } }; } -function getMockContext () { - return { - done: sinon.stub(), - success: sinon.stub(), - failure: sinon.stub() - }; -} - -describe('functions:ocr', function () { - it('processImage does nothing on delete', function () { - var context = getMockContext(); - - getSample().sample.processImage(context, { - timeDeleted: 1234 - }); - - assert.equal(context.done.calledOnce, true); - assert.equal(context.failure.called, false); - assert.equal(context.success.called, false); +describe(`functions:ocr`, () => { + it(`processImage does nothing on delete`, () => { + return getSample().program.processImage({ data: { resourceState: `not_exists` } }); }); - it('processImage fails without a bucket', function () { - var expectedMsg = 'Bucket not provided. Make sure you have a ' + - '"bucket" property in your request'; - var context = getMockContext(); + it(`processImage fails without a bucket`, () => { + const error = new Error(`Bucket not provided. Make sure you have a "bucket" property in your request`); - getSample().sample.processImage(context, {}); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.called, true); + return getSample().program.processImage({ data: {} }) + .catch((err) => { + assert.deepEqual(err, error); + }); }); - it('processImage fails without a name', function () { - var expectedMsg = 'Filename not provided. Make sure you have a ' + - '"name" property in your request'; - var context = getMockContext(); - - getSample().sample.processImage(context, { - bucket: bucket - }); + it(`processImage fails without a name`, () => { + const error = new Error(`Filename not provided. Make sure you have a "name" property in your request`); - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.called, true); + return getSample().program.processImage({ data: { bucket: bucketName } }) + .catch((err) => { + assert.deepEqual(err, error); + }); }); - it('processImage handles detectText error', function (done) { - var expectedMsg = 'error'; - var context = { - success: assert.fail, - failure: function () { - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.calledWith(expectedMsg), true); - done(); + it(`processImage processes an image`, () => { + const event = { + data: { + bucket: bucketName, + name: filename } }; - - sinon.spy(context, 'success'); - sinon.spy(context, 'failure'); - - var ocrSample = getSample(); - ocrSample.mocks.vision.detectText = sinon.stub().callsArgWith(1, expectedMsg); - ocrSample.sample.processImage(context, { - bucket: bucket, - name: name - }); - }); - - it('processImage processes an image', function (done) { - var context = { - success: function () { - assert.equal(context.success.calledOnce, true); - assert.equal(context.failure.called, false); - assert.equal(console.log.calledWith('Processed ' + name), true); - done(); - }, - failure: assert.fail - }; - - sinon.spy(context, 'success'); - sinon.spy(context, 'failure'); - - var ocrSample = getSample(); - ocrSample.mocks.vision.detectText = sinon.stub().callsArgWith(1, null, [ - text - ], {}); - ocrSample.mocks.translate.detect = sinon.stub().callsArgWith(1, null, { - language: 'ja' - }); - ocrSample.sample.processImage(context, { - bucket: bucket, - name: name - }); - }); - - it('translateText fails without text', function () { - var expectedMsg = 'Text not provided. Make sure you have a ' + - '"text" property in your request'; - var context = getMockContext(); - - getSample().sample.translateText(context, {}); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.called, true); - }); - - it('translateText fails without a filename', function () { - var expectedMsg = 'Filename not provided. Make sure you have a ' + - '"filename" property in your request'; - var context = getMockContext(); - - getSample().sample.translateText(context, { - text: text - }); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.called, true); + const sample = getSample(); + + return sample.program.processImage(event) + .then(() => { + assert.equal(console.log.callCount, 4); + assert.deepEqual(console.log.getCall(0).args, [`Looking for text in image ${filename}`]); + assert.deepEqual(console.log.getCall(1).args, [`Extracted text from image (${text.length} chars)`]); + assert.deepEqual(console.log.getCall(2).args, [`Detected language "ja" for ${filename}`]); + assert.deepEqual(console.log.getCall(3).args, [`File ${event.data.name} processed.`]); + }); }); - it('translateText fails without a lang', function () { - var expectedMsg = 'Language not provided. Make sure you have a ' + - '"lang" property in your request'; - var context = getMockContext(); - - getSample().sample.translateText(context, { - text: text, - filename: filename - }); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.called, true); - }); - - it('translateText handles translation error', function (done) { - var expectedMsg = 'error'; - var context = { - success: assert.fail, - failure: function () { - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.calledWith(expectedMsg), true); - done(); + it(`translateText fails without text`, () => { + const error = new Error(`Text not provided. Make sure you have a "text" property in your request`); + const event = { + data: { + data: Buffer.from(JSON.stringify({})).toString(`base64`) } }; - sinon.spy(context, 'success'); - sinon.spy(context, 'failure'); - - var ocrSample = getSample(); - ocrSample.mocks.translate.translate = sinon.stub().callsArgWith(2, expectedMsg); - ocrSample.sample.translateText(context, { - text: text, - filename: filename, - lang: lang - }); + return getSample().program.translateText(event) + .catch((err) => { + assert.deepEqual(err, error); + }); }); - it('translateText handles get topic error', function (done) { - var expectedMsg = 'error'; - var context = { - success: assert.fail, - failure: function () { - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.calledWith(expectedMsg), true); - done(); + it(`translateText fails without a filename`, () => { + const error = new Error(`Filename not provided. Make sure you have a "filename" property in your request`); + const event = { + data: { + data: Buffer.from(JSON.stringify({ text })).toString(`base64`) } }; - sinon.spy(context, 'success'); - sinon.spy(context, 'failure'); - - var ocrSample = getSample(); - ocrSample.mocks.translate.translate = sinon.stub().callsArgWith(2, null, translation); - ocrSample.mocks.topic.get = sinon.stub().callsArgWith(1, expectedMsg); - ocrSample.sample.translateText(context, { - text: text, - filename: filename, - lang: lang - }); + return getSample().program.translateText(event) + .catch((err) => { + assert.deepEqual(err, error); + }); }); - it('translateText handles publish error', function (done) { - var expectedMsg = 'error'; - var context = { - success: assert.fail, - failure: function () { - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.calledWith(expectedMsg), true); - done(); + it(`translateText fails without a lang`, () => { + const error = new Error(`Language not provided. Make sure you have a "lang" property in your request`); + const event = { + data: { + data: Buffer.from(JSON.stringify({ text, filename })).toString(`base64`) } }; - sinon.spy(context, 'success'); - sinon.spy(context, 'failure'); - - var ocrSample = getSample(); - ocrSample.mocks.translate.translate = sinon.stub().callsArgWith(2, null, translation); - ocrSample.mocks.topic.publish = sinon.stub().callsArgWith(1, expectedMsg); - ocrSample.sample.translateText(context, { - text: text, - filename: filename, - lang: lang - }); + return getSample().program.translateText(event) + .catch((err) => { + assert.deepEqual(err, error); + }); }); - it('translateText translates and publishes text', function (done) { - var context = { - success: function () { - assert.equal(context.success.called, true); - assert.equal(context.failure.called, false); - assert.equal(console.log.calledWith('Text translated to ' + lang), true); - done(); - }, - failure: assert.fail + it(`translateText translates and publishes text`, () => { + const event = { + data: { + data: Buffer.from( + JSON.stringify({ + text, + filename, + lang + }) + ).toString(`base64`) + } }; + const sample = getSample(); - sinon.spy(context, 'success'); - sinon.spy(context, 'failure'); + sample.mocks.translate.translate.returns(Promise.resolve([translation])); - var ocrSample = getSample(); - ocrSample.mocks.translate.translate = sinon.stub().callsArgWith(2, null, translation); - ocrSample.sample.translateText(context, { - text: text, - filename: filename, - lang: lang - }); + return sample.program.translateText(event) + .then(() => { + assert.equal(console.log.callCount, 2); + assert.deepEqual(console.log.firstCall.args, [`Translating text into ${lang}`]); + assert.deepEqual(console.log.secondCall.args, [`Text translated to ${lang}`]); + }); }); - it('saveResult fails without text', function () { - var expectedMsg = 'Text not provided. Make sure you have a ' + - '"text" property in your request'; - var context = getMockContext(); - - getSample().sample.saveResult(context, {}); - - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.called, true); - }); - - it('saveResult fails without a filename', function () { - var expectedMsg = 'Filename not provided. Make sure you have a ' + - '"filename" property in your request'; - var context = getMockContext(); - - getSample().sample.saveResult(context, { - text: text - }); + it(`saveResult fails without text`, () => { + const error = new Error(`Text not provided. Make sure you have a "text" property in your request`); + const event = { + data: { + data: Buffer.from(JSON.stringify({})).toString(`base64`) + } + }; - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.called, true); + return getSample().program.saveResult(event) + .catch((err) => { + assert.deepEqual(err, error); + }); }); - it('saveResult fails without a lang', function () { - var expectedMsg = 'Language not provided. Make sure you have a ' + - '"lang" property in your request'; - var context = getMockContext(); - - getSample().sample.saveResult(context, { - text: text, - filename: filename - }); + it(`saveResult fails without a filename`, () => { + const error = new Error(`Filename not provided. Make sure you have a "filename" property in your request`); + const event = { + data: { + data: Buffer.from(JSON.stringify({ text })).toString(`base64`) + } + }; - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.called, true); + return getSample().program.saveResult(event) + .catch((err) => { + assert.deepEqual(err, error); + }); }); - it('saveResult handles save error', function (done) { - var expectedMsg = 'error'; - var context = { - success: assert.fail, - failure: function () { - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); - assert.equal(console.error.calledWith(expectedMsg), true); - done(); + it(`saveResult fails without a lang`, () => { + const error = new Error(`Language not provided. Make sure you have a "lang" property in your request`); + const event = { + data: { + data: Buffer.from(JSON.stringify({ text, filename })).toString(`base64`) } }; - sinon.spy(context, 'success'); - sinon.spy(context, 'failure'); - - var ocrSample = getSample(); - ocrSample.mocks.file.save = sinon.stub().callsArgWith(1, expectedMsg); - ocrSample.sample.saveResult(context, { - text: text, - filename: filename, - lang: lang - }); + return getSample().program.saveResult(event) + .catch((err) => { + assert.deepEqual(err, error); + }); }); - it('saveResult translates and publishes text', function (done) { - var context = { - success: function () { - assert.equal(context.success.called, true); - assert.equal(context.failure.called, false); - assert.equal(console.log.calledWith('Text written to ' + filename + '_to_lang.txt'), true); - done(); - }, - failure: assert.fail + it(`saveResult translates and publishes text`, () => { + const event = { + data: { + data: Buffer.from(JSON.stringify({ text, filename, lang })).toString(`base64`) + } }; - - sinon.spy(context, 'success'); - sinon.spy(context, 'failure'); - - var ocrSample = getSample(); - ocrSample.sample.saveResult(context, { - text: text, - filename: filename, - lang: lang - }); + const sample = getSample(); + + return sample.program.saveResult(event) + .then(() => { + assert.equal(console.log.callCount, 3); + assert.deepEqual(console.log.getCall(0).args, [`Received request to save file ${filename}`]); + assert.deepEqual(console.log.getCall(1).args, [`Saving result to ${filename}_to_${lang}.txt in bucket ${sample.mocks.config.RESULT_BUCKET}`]); + assert.deepEqual(console.log.getCall(2).args, [`File saved.`]); + }); }); - it('saveResult translates and publishes text with dot in filename', function (done) { - var context = { - success: function () { - assert.equal(context.success.called, true); - assert.equal(context.failure.called, false); - assert.equal(console.log.calledWith('Text written to ' + filename + '_to_lang.txt'), true); - done(); - }, - failure: assert.fail + it(`saveResult translates and publishes text with dot in filename`, () => { + const event = { + data: { + data: Buffer.from(JSON.stringify({ text, filename: `${filename}.jpg`, lang })).toString(`base64`) + } }; - - sinon.spy(context, 'success'); - sinon.spy(context, 'failure'); - - var ocrSample = getSample(); - ocrSample.sample.saveResult(context, { - text: text, - filename: filename + '.jpg', - lang: lang - }); + const sample = getSample(); + + return sample.program.saveResult(event) + .then(() => { + assert.equal(console.log.callCount, 3); + assert.deepEqual(console.log.getCall(0).args, [`Received request to save file ${filename}.jpg`]); + assert.deepEqual(console.log.getCall(1).args, [`Saving result to ${filename}.jpg_to_${lang}.txt in bucket ${sample.mocks.config.RESULT_BUCKET}`]); + assert.deepEqual(console.log.getCall(2).args, [`File saved.`]); + }); }); }); diff --git a/functions/pubsub/README.md b/functions/pubsub/README.md index 034b6b4623..3cbbe55385 100644 --- a/functions/pubsub/README.md +++ b/functions/pubsub/README.md @@ -22,34 +22,34 @@ Functions for your project. 1. Create a Cloud Pub/Sub topic (if you already have one you want to use, you can skip this step): - gcloud alpha pubsub topics create [YOUR_TOPIC_NAME] + gcloud beta pubsub topics create YOUR_TOPIC_NAME - * Replace `[YOUR_TOPIC_NAME]` with the name of your Pub/Sub Topic. + * Replace `YOUR_TOPIC_NAME` with the name of your Pub/Sub Topic. 1. Create a Cloud Storage Bucket to stage our deployment: - gsutil mb gs://[YOUR_BUCKET_NAME] + gsutil mb gs://YOUR_BUCKET_NAME - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. + * Replace `YOUR_BUCKET_NAME` with the name of your Cloud Storage Bucket. 1. Deploy the `publish` function with an HTTP trigger: - gcloud alpha functions deploy publish --bucket [YOUR_BUCKET_NAME] --trigger-http + gcloud alpha functions deploy publish --stage-bucket YOUR_BUCKET_NAME --trigger-http - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. + * Replace `YOUR_BUCKET_NAME` with the name of your Cloud Storage Bucket. 1. Deploy the `subscribe` function with the Pub/Sub topic as a trigger: - gcloud alpha functions deploy subscribe --bucket [YOUR_BUCKET_NAME] --trigger-topic [YOUR_TOPIC_NAME] + gcloud alpha functions deploy subscribe --stage-bucket YOUR_BUCKET_NAME --trigger-topic YOUR_TOPIC_NAME - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. - * Replace `[YOUR_TOPIC_NAME]` with the name of your Pub/Sub Topic. + * Replace `YOUR_BUCKET_NAME` with the name of your Cloud Storage Bucket. + * Replace `YOUR_TOPIC_NAME` with the name of your Pub/Sub Topic. 1. Call the `publish` function: - gcloud alpha functions call publish --data '{"topic":"[YOUR_TOPIC_NAME]","message":"Hello World!"}' + gcloud alpha functions call publish --data '{"topic":"YOUR_TOPIC_NAME","message":"Hello World!"}' - * Replace `[YOUR_TOPIC_NAME]` with the name of your Pub/Sub Topic. + * Replace `YOUR_TOPIC_NAME` with the name of your Pub/Sub Topic. 1. Check the logs for the `subscribe` function: diff --git a/functions/pubsub/index.js b/functions/pubsub/index.js index 513c8fc0d4..aeee11a7d6 100644 --- a/functions/pubsub/index.js +++ b/functions/pubsub/index.js @@ -1,83 +1,88 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var PubSub = require('@google-cloud/pubsub'); +// [START functions_pubsub_setup] +const PubSub = require('@google-cloud/pubsub'); -// Instantiate a pubsub client -var pubsub = PubSub(); +// Instantiates a client +const pubsub = PubSub(); +// [END functions_pubsub_setup] +// [START functions_pubsub_publish] /** * Publishes a message to a Cloud Pub/Sub Topic. * * @example - * gcloud alpha functions call publish --data '{"topic":"","message":"Hello World!"}' + * gcloud alpha functions call publish --data '{"topic":"[YOUR_TOPIC_NAME]","message":"Hello, world!"}' + * + * - Replace `[YOUR_TOPIC_NAME]` with your Cloud Pub/Sub topic name. * - * @param {Object} context Cloud Function context. - * @param {Function} context.success Success callback. - * @param {Function} context.failure Failure callback. - * @param {Object} data Request data, in this case an object provided by the user. - * @param {string} data.topic Topic name on which to publish. - * @param {string} data.message Message to publish. + * @param {object} req Cloud Function request context. + * @param {object} req.body The request body. + * @param {string} req.body.topic Topic name on which to publish. + * @param {string} req.body.message Message to publish. + * @param {object} res Cloud Function response context. */ -exports.publish = function publish (context, data) { - try { - if (!data.topic) { - throw new Error('Topic not provided. Make sure you have a ' + - '"topic" property in your request'); - } - if (!data.message) { - throw new Error('Message not provided. Make sure you have a ' + - '"message" property in your request'); - } +exports.publish = function publish (req, res) { + if (!req.body.topic) { + res.status(500).send(new Error('Topic not provided. Make sure you have a "topic" property in your request')); + return; + } else if (!req.body.message) { + res.status(500).send(new Error('Message not provided. Make sure you have a "message" property in your request')); + return; + } + + console.log(`Publishing message to topic ${req.body.topic}`); - console.log('Publishing message to topic ' + data.topic); + // References an existing topic + const topic = pubsub.topic(req.body.topic); - // The Pub/Sub topic must already exist. - var topic = pubsub.topic(data.topic); + const message = { + data: { + message: req.body.message + } + }; - // Pub/Sub messages must be valid JSON objects. - return topic.publish({ - data: { - message: data.message - } - }, function (err) { - if (err) { - console.error(err); - return context.failure(err); - } - return context.success('Message published'); + // Publishes a message + return topic.publish(message) + .then(() => res.status(200).send('Message published.')) + .catch((err) => { + console.error(err); + res.status(500).send(err); }); - } catch (err) { - console.error(err); - return context.failure(err.message); - } }; +// [END functions_pubsub_publish] +// [START functions_pubsub_subscribe] /** - * Triggered from a message on a Pub/Sub topic. + * Triggered from a message on a Cloud Pub/Sub topic. * - * @param {Object} context Cloud Function context. - * @param {Function} context.success Success callback. - * @param {Function} context.failure Failure callback. - * @param {Object} data Request data, in this case an object provided by the Pub/Sub trigger. - * @param {Object} data.message Message that was published via Pub/Sub. + * @param {object} event The Cloud Functions event. + * @param {object} event.data The Cloud Pub/Sub Message object. + * @param {string} event.data.data The "data" property of the Cloud Pub/Sub Message. + * @param {function} The callback function. */ -exports.subscribe = function subscribe (context, data) { +exports.subscribe = function subscribe (event, callback) { + const pubsubMessage = event.data; + // We're just going to log the message to prove that it worked! - console.log(data.message); + console.log(Buffer.from(pubsubMessage.data, 'base64').toString()); - // Don't forget to call success! - context.success(); + // Don't forget to call the callback! + callback(); }; +// [END functions_pubsub_subscribe] diff --git a/functions/pubsub/package.json b/functions/pubsub/package.json index 927c4f9aa4..5398da1697 100644 --- a/functions/pubsub/package.json +++ b/functions/pubsub/package.json @@ -6,12 +6,9 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- functions/pubsub/test/*.test.js" }, "dependencies": { - "@google-cloud/pubsub": "^0.1.1" - }, - "devDependencies": { - "mocha": "^3.0.2" + "@google-cloud/pubsub": "^0.5.0" } } diff --git a/functions/pubsub/test/index.test.js b/functions/pubsub/test/index.test.js index aaea5a0370..6dd81a4ed9 100644 --- a/functions/pubsub/test/index.test.js +++ b/functions/pubsub/test/index.test.js @@ -1,141 +1,137 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var proxyquire = require('proxyquire').noCallThru(); +const proxyquire = require(`proxyquire`).noCallThru(); + +const TOPIC = `topic`; +const MESSAGE = `Hello, world!`; function getSample () { - var topicMock = { - publish: sinon.stub().callsArg(1) + const topicMock = { + publish: sinon.stub().returns(Promise.resolve()) }; - var pubsubMock = { + const pubsubMock = { topic: sinon.stub().returns(topicMock) }; - var PubSubMock = sinon.stub().returns(pubsubMock); + const PubSubMock = sinon.stub().returns(pubsubMock); + return { - sample: proxyquire('../', { + program: proxyquire(`../`, { '@google-cloud/pubsub': PubSubMock }), mocks: { PubSub: PubSubMock, pubsub: pubsubMock, - topic: topicMock + topic: topicMock, + req: { + body: { + topic: TOPIC, + message: MESSAGE + } + }, + res: { + status: sinon.stub().returnsThis(), + send: sinon.stub().returnsThis() + } } }; } -function getMockContext () { - return { - success: sinon.stub(), - failure: sinon.stub() - }; -} - -describe('functions:pubsub', function () { - it('Publish fails without a topic', function () { - var expectedMsg = 'Topic not provided. Make sure you have a "topic" ' + - 'property in your request'; - var context = getMockContext(); +describe(`functions:pubsub`, () => { + it(`Publish fails without a topic`, () => { + const expectedMsg = `Topic not provided. Make sure you have a "topic" property in your request`; + const sample = getSample(); - getSample().sample.publish(context, { - message: 'message' - }); + delete sample.mocks.req.body.topic; + sample.program.publish(sample.mocks.req, sample.mocks.res); - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); + assert.deepEqual(sample.mocks.res.status.callCount, 1); + assert.deepEqual(sample.mocks.res.status.firstCall.args, [500]); + assert.deepEqual(sample.mocks.res.send.callCount, 1); + assert.equal(sample.mocks.res.send.firstCall.args[0].message, expectedMsg); }); - it('Publish fails without a message', function () { - var expectedMsg = 'Message not provided. Make sure you have a "message" ' + - 'property in your request'; - var context = getMockContext(); + it(`Publish fails without a message`, () => { + const expectedMsg = `Message not provided. Make sure you have a "message" property in your request`; + const sample = getSample(); - getSample().sample.publish(context, { - topic: 'topic' - }); + delete sample.mocks.req.body.message; + sample.program.publish(sample.mocks.req, sample.mocks.res); - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(context.success.called, false); + assert.deepEqual(sample.mocks.res.status.callCount, 1); + assert.deepEqual(sample.mocks.res.status.firstCall.args, [500]); + assert.deepEqual(sample.mocks.res.send.callCount, 1); + assert.equal(sample.mocks.res.send.firstCall.args[0].message, expectedMsg); }); - it('Publishes the message to the topic and calls success', function () { - var expectedMsg = 'Message published'; - var data = { - topic: 'topic', - message: 'message' - }; - var context = getMockContext(); - - var pubsubSample = getSample(); - pubsubSample.sample.publish(context, data); - - assert.equal(context.success.calledOnce, true); - assert.equal(context.success.firstCall.args[0], expectedMsg); - assert.equal(context.failure.called, false); - assert.equal(pubsubSample.mocks.pubsub.topic.calledOnce, true); - assert.deepEqual(pubsubSample.mocks.pubsub.topic.firstCall.args[0], data.topic); - assert.equal(pubsubSample.mocks.topic.publish.calledOnce, true); - assert.deepEqual(pubsubSample.mocks.topic.publish.firstCall.args[0], { - data: { - message: data.message - } - }); + it(`Publishes the message to the topic and calls success`, () => { + const expectedMsg = `Message published.`; + const sample = getSample(); + + return sample.program.publish(sample.mocks.req, sample.mocks.res) + .then(() => { + assert.deepEqual(sample.mocks.topic.publish.callCount, 1); + assert.deepEqual(sample.mocks.topic.publish.firstCall.args, [{ + data: { + message: MESSAGE + } + }]); + assert.deepEqual(sample.mocks.res.status.callCount, 1); + assert.deepEqual(sample.mocks.res.status.firstCall.args, [200]); + assert.deepEqual(sample.mocks.res.send.callCount, 1); + assert.deepEqual(sample.mocks.res.send.firstCall.args, [expectedMsg]); + }); }); - it('Fails to publish the message and calls failure', function () { - var expectedMsg = 'error'; - var data = { - topic: 'topic', - message: 'message' - }; - var context = getMockContext(); - - var pubsubSample = getSample(); - pubsubSample.mocks.topic.publish = sinon.stub().callsArgWith(1, expectedMsg); - - pubsubSample.sample.publish(context, data); + it(`Fails to publish the message and calls failure`, () => { + const error = new Error(`error`); + const sample = getSample(); + sample.mocks.topic.publish.returns(Promise.reject(error)); + + return sample.program.publish(sample.mocks.req, sample.mocks.res) + .then(() => { + throw new Error(`Should have failed!`); + }) + .catch((err) => { + assert.deepEqual(err, error); + assert.deepEqual(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + assert.deepEqual(sample.mocks.res.status.callCount, 1); + assert.deepEqual(sample.mocks.res.status.firstCall.args, [500]); + assert.deepEqual(sample.mocks.res.send.callCount, 1); + assert.deepEqual(sample.mocks.res.send.firstCall.args, [error]); + }); + }); - assert.equal(context.success.called, false); - assert.equal(context.failure.calledOnce, true); - assert.equal(context.failure.firstCall.args[0], expectedMsg); - assert.equal(pubsubSample.mocks.pubsub.topic.calledOnce, true); - assert.deepEqual(pubsubSample.mocks.pubsub.topic.firstCall.args[0], data.topic); - assert.equal(pubsubSample.mocks.topic.publish.calledOnce, true); - assert.deepEqual(pubsubSample.mocks.topic.publish.firstCall.args[0], { + it(`Subscribes to a message`, () => { + const callback = sinon.stub(); + const json = JSON.stringify({ data: MESSAGE }); + const event = { data: { - message: data.message + data: Buffer.from(json).toString('base64') } - }); - }); - - it('Subscribes to a message', function () { - var expectedMsg = 'message'; - var data = { - topic: 'topic', - message: expectedMsg }; - var context = getMockContext(); - - var pubsubSample = getSample(); - pubsubSample.sample.subscribe(context, data); + const sample = getSample(); + sample.program.subscribe(event, callback); - assert.equal(console.log.called, true); - assert.equal(console.log.calledWith(expectedMsg), true); - assert.equal(context.success.calledOnce, true); - assert.equal(context.failure.called, false); + assert.deepEqual(console.log.callCount, 1); + assert.deepEqual(console.log.firstCall.args, [json]); + assert.deepEqual(callback.callCount, 1); + assert.deepEqual(callback.firstCall.args, []); }); }); diff --git a/functions/sendgrid/.gitignore b/functions/sendgrid/.gitignore index 254077b7f3..11dc9156b6 100644 --- a/functions/sendgrid/.gitignore +++ b/functions/sendgrid/.gitignore @@ -1,4 +1,3 @@ node_modules config.json test.js -test/ \ No newline at end of file diff --git a/functions/sendgrid/index.js b/functions/sendgrid/index.js index aa514506c6..a8327a2499 100644 --- a/functions/sendgrid/index.js +++ b/functions/sendgrid/index.js @@ -1,101 +1,105 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -// [START setup] -var async = require('async'); -var sendgrid = require('sendgrid'); -var config = require('./config.json'); -var gcloud = require('google-cloud'); -var uuid = require('node-uuid'); +// [START functions_sendgrid_setup] +const sendgrid = require('sendgrid'); +const config = require('./config.json'); +const uuid = require('node-uuid'); // Get a reference to the Cloud Storage component -var storage = gcloud.storage(); +const storage = require('@google-cloud/storage')(); // Get a reference to the BigQuery component -var bigquery = gcloud.bigquery(); -// [END setup] +const bigquery = require('@google-cloud/bigquery')(); +// [END functions_sendgrid_setup] -// [START getClient] +// [START functions_sendgrid_get_client] /** * Returns a configured SendGrid client. * * @param {string} key Your SendGrid API key. - * @returns {Object} SendGrid client. + * @returns {object} SendGrid client. */ function getClient (key) { if (!key) { - var error = new Error('SendGrid API key not provided. Make sure you have a ' + - '"sg_key" property in your request querystring'); + const error = new Error('SendGrid API key not provided. Make sure you have a "sg_key" property in your request querystring'); error.code = 401; throw error; } // Using SendGrid's Node.js Library https://github.com/sendgrid/sendgrid-nodejs - return sendgrid.SendGrid(key); + return sendgrid(key); } -// [END getClient] +// [END functions_sendgrid_get_client] -// [START getPayload] +// [START functions_get_payload] /** * Constructs the SendGrid email request from the HTTP request body. * - * @param {Object} requestBody Cloud Function request body. + * @param {object} requestBody Cloud Function request body. * @param {string} data.to Email address of the recipient. * @param {string} data.from Email address of the sender. * @param {string} data.subject Email subject line. * @param {string} data.body Body of the email subject line. - * @returns {Object} Payload object. + * @returns {object} Payload object. */ function getPayload (requestBody) { if (!requestBody.to) { - var error = new Error('To email address not provided. Make sure you have a ' + - '"to" property in your request'); + const error = new Error('To email address not provided. Make sure you have a "to" property in your request'); error.code = 400; throw error; - } - - if (!requestBody.from) { - error = new Error('From email address not provided. Make sure you have a ' + - '"from" property in your request'); + } else if (!requestBody.from) { + const error = new Error('From email address not provided. Make sure you have a "from" property in your request'); error.code = 400; throw error; - } - - if (!requestBody.subject) { - error = new Error('Email subject line not provided. Make sure you have a ' + - '"subject" property in your request'); + } else if (!requestBody.subject) { + const error = new Error('Email subject line not provided. Make sure you have a "subject" property in your request'); error.code = 400; throw error; - } - - if (!requestBody.body) { - error = new Error('Email content not provided. Make sure you have a ' + - '"body" property in your request'); + } else if (!requestBody.body) { + const error = new Error('Email content not provided. Make sure you have a "body" property in your request'); error.code = 400; throw error; } - return new sendgrid.mail.Mail( - new sendgrid.mail.Email(requestBody.from), - requestBody.subject, - new sendgrid.mail.Email(requestBody.to), - new sendgrid.mail.Content('text/plain', requestBody.body) - ); + return { + personalizations: [ + { + to: [ + { + email: requestBody.to + } + ], + subject: requestBody.subject + } + ], + from: { + email: requestBody.from + }, + content: [ + { + type: 'text/plain', + value: requestBody.body + } + ] + }; } -// [END getPayload] +// [END functions_get_payload] -// [START email] +// [START functions_sendgrid_email] /** * Send an email using SendGrid. * @@ -105,42 +109,48 @@ function getPayload (requestBody) { * @example * curl -X POST "https://us-central1.your-project-id.cloudfunctions.net/sendEmail?sg_key=your_api_key" --data '{"to":"bob@email.com","from":"alice@email.com","subject":"Hello from Sendgrid!","body":"Hello World!"}' --header "Content-Type: application/json" * - * @param {Object} req Cloud Function request context. - * @param {Object} req.query The parsed querystring. + * @param {object} req Cloud Function request context. + * @param {object} req.query The parsed querystring. * @param {string} req.query.sg_key Your SendGrid API key. - * @param {Object} req.body The request payload. + * @param {object} req.body The request payload. * @param {string} req.body.to Email address of the recipient. * @param {string} req.body.from Email address of the sender. * @param {string} req.body.subject Email subject line. * @param {string} req.body.body Body of the email subject line. - * @param {Object} res Cloud Function response context. + * @param {object} res Cloud Function response context. */ exports.sendgridEmail = function sendgridEmail (req, res) { - try { - if (req.method !== 'POST') { - var error = new Error('Only POST requests are accepted'); - error.code = 405; - throw error; - } + return Promise.resolve() + .then(() => { + if (req.method !== 'POST') { + const error = new Error('Only POST requests are accepted'); + error.code = 405; + throw error; + } - // Get a SendGrid client - var client = getClient(req.query.sg_key); + // Get a SendGrid client + const client = getClient(req.query.sg_key); - // Build the SendGrid request to send email - var request = client.emptyRequest(); - request.method = 'POST'; - request.path = '/v3/mail/send'; - request.body = getPayload(req.body).toJSON(); + // Build the SendGrid request to send email + const request = client.emptyRequest({ + method: 'POST', + path: '/v3/mail/send', + body: getPayload(req.body) + }); - // Make the request to SendGrid's API - console.log('Sending email to: ' + req.body.to); - client.API(request, function (response) { + // Make the request to SendGrid's API + console.log(`Sending email to: ${req.body.to}`); + return client.API(request); + }) + .then((response) => { if (response.statusCode < 200 || response.statusCode >= 400) { - console.error(response); - } else { - console.log('Email sent to: ' + req.body.to); + const error = Error(response.body); + error.code = response.statusCode; + throw error; } + console.log(`Email sent to: ${req.body.to}`); + // Forward the response back to the requester res.status(response.statusCode); if (response.headers['content-type']) { @@ -154,32 +164,33 @@ exports.sendgridEmail = function sendgridEmail (req, res) { } else { res.end(); } + }) + .catch((err) => { + console.error(err); + const code = err.code || (err.response ? err.response.statusCode : 500) || 500; + res.status(code).send(err); }); - } catch (err) { - console.error(err); - return res.status(err.code || 500).send(err.message); - } }; -// [END email] +// [END functions_sendgrid_email] -// [START verifyWebhook] +// [START functions_sendgrid_verify_webhook] /** * Verify that the webhook request came from sendgrid. * * @param {string} authorization The authorization header of the request, e.g. "Basic ZmdvOhJhcg==" */ function verifyWebhook (authorization) { - var basicAuth = new Buffer(authorization.replace('Basic ', ''), 'base64').toString(); - var parts = basicAuth.split(':'); + const basicAuth = new Buffer(authorization.replace('Basic ', ''), 'base64').toString(); + const parts = basicAuth.split(':'); if (parts[0] !== config.USERNAME || parts[1] !== config.PASSWORD) { - var error = new Error('Invalid credentials'); + const error = new Error('Invalid credentials'); error.code = 401; throw error; } } -// [END verifyWebhook] +// [END functions_sendgrid_verify_webhook] -// [START fixNames] +// [START functions_sendgrid_fix_names] /** * Recursively rename properties in to meet BigQuery field name requirements. * @@ -189,173 +200,127 @@ function fixNames (obj) { if (Array.isArray(obj)) { obj.forEach(fixNames); } else if (obj && typeof obj === 'object') { - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - var value = obj[key]; - fixNames(value); - var fixedKey = key.replace('-', '_'); - if (fixedKey !== key) { - obj[fixedKey] = value; - delete obj[key]; - } + Object.keys(obj).forEach((key) => { + const value = obj[key]; + fixNames(value); + const fixedKey = key.replace('-', '_'); + if (fixedKey !== key) { + obj[fixedKey] = value; + delete obj[key]; } - } + }); } } -// [END fixNames] +// [END functions_sendgrid_fix_names] -// [START webhook] +// [START functions_sendgrid_webhook] /** * Receive a webhook from SendGrid. * * See https://sendgrid.com/docs/API_Reference/Webhooks/event.html * - * @param {Object} req Cloud Function request context. - * @param {Object} res Cloud Function response context. + * @param {object} req Cloud Function request context. + * @param {object} res Cloud Function response context. */ exports.sendgridWebhook = function sendgridWebhook (req, res) { - try { - if (req.method !== 'POST') { - var error = new Error('Only POST requests are accepted'); - error.code = 405; - throw error; - } - - verifyWebhook(req.get('authorization') || ''); + return Promise.resolve() + .then(() => { + if (req.method !== 'POST') { + const error = new Error('Only POST requests are accepted'); + error.code = 405; + throw error; + } - var events = req.body || []; + verifyWebhook(req.get('authorization') || ''); - // Make sure property names in the data meet BigQuery standards - fixNames(events); + const events = req.body || []; - // Generate newline-delimited JSON - // See https://cloud.google.com/bigquery/data-formats#json_format - var json = events.map(function (event) { - return JSON.stringify(event); - }).join('\n'); + // Make sure property names in the data meet BigQuery standards + fixNames(events); - // Upload a new file to Cloud Storage if we have events to save - if (json.length) { - var bucketName = config.EVENT_BUCKET; - var unixTimestamp = new Date().getTime() * 1000; - var filename = '' + unixTimestamp + '-' + uuid.v4() + '.json'; - var file = storage.bucket(bucketName).file(filename); + // Generate newline-delimited JSON + // See https://cloud.google.com/bigquery/data-formats#json_format + const json = events.map((event) => JSON.stringify(event)).join('\n'); - console.log('Saving events to ' + filename + ' in bucket ' + bucketName); + // Upload a new file to Cloud Storage if we have events to save + if (json.length) { + const bucketName = config.EVENT_BUCKET; + const unixTimestamp = new Date().getTime() * 1000; + const filename = `${unixTimestamp}-${uuid.v4()}.json`; + const file = storage.bucket(bucketName).file(filename); - return file.save(json, function (err) { - if (err) { - console.error(err); - return res.status(500).end(); - } - console.log('JSON written to ' + filename); - return res.status(200).end(); - }); - } + console.log(`Saving events to ${filename} in bucket ${bucketName}`); - return res.status(200).end(); - } catch (err) { - console.error(err); - return res.status(err.code || 500).send(err.message); - } + return file.save(json).then(() => { + console.log(`JSON written to ${filename}`); + }); + } + }) + .then(() => res.status(200).end()) + .catch((err) => { + console.error(err); + res.status(err.code || 500).send(err); + }); }; -// [END webhook] +// [END functions_sendgrid_webhook] -// [START getTable] +// [START functions_sendgrid_get_table] /** * Helper method to get a handle on a BigQuery table. Automatically creates the * dataset and table if necessary. - * - * @param {Function} callback Callback function. */ -function getTable (callback) { - var dataset = bigquery.dataset(config.DATASET); - return dataset.get({ - autoCreate: true - }, function (err, dataset) { - if (err) { - return callback(err); - } - var table = dataset.table(config.TABLE); - return table.get({ - autoCreate: true - }, function (err, table) { - if (err) { - return callback(err); - } - return callback(null, table); - }); - }); +function getTable () { + const dataset = bigquery.dataset(config.DATASET); + const options = { autoCreate: true }; + + return dataset.get(options) + .then(([dataset]) => dataset.table(config.TABLE).get(options)); } -// [END getTable] +// [END functions_sendgrid_get_table] -// [START load] +// [START functions_sendgrid_load] /** * Cloud Function triggered by Cloud Storage when a file is uploaded. * - * @param {Object} context Cloud Function context. - * @param {Function} context.success Success callback. - * @param {Function} context.failure Failure callback. - * @param {Object} data Request data, in this case an object provided by Cloud Storage. - * @param {string} data.bucket Name of the Cloud Storage bucket. - * @param {string} data.name Name of the file. - * @param {string} [data.timeDeleted] Time the file was deleted if this is a deletion event. + * @param {object} event The Cloud Functions event. + * @param {object} event.data A Cloud Storage file object. + * @param {string} event.data.bucket Name of the Cloud Storage bucket. + * @param {string} event.data.name Name of the file. + * @param {string} [event.data.timeDeleted] Time the file was deleted if this is a deletion event. * @see https://cloud.google.com/storage/docs/json_api/v1/objects#resource */ -exports.sendgridLoad = function sendgridLoad (context, data) { - try { - if (data.hasOwnProperty('timeDeleted')) { - // This was a deletion event, we don't want to process this - return context.done(); - } - - if (!data.bucket) { - throw new Error('Bucket not provided. Make sure you have a ' + - '"bucket" property in your request'); - } - if (!data.name) { - throw new Error('Filename not provided. Make sure you have a ' + - '"name" property in your request'); - } +exports.sendgridLoad = function sendgridLoad (event) { + const file = event.data; - return async.waterfall([ - // Get a handle on the table - function (callback) { - getTable(callback); - }, - // Start the load job - function (table, callback) { - console.log('Starting job for ' + data.name); + if (file.resourceState === 'not_exists') { + // This was a deletion event, we don't want to process this + return; + } - var file = storage.bucket(data.bucket).file(data.name); - var metadata = { - autodetect: true, - sourceFormat: 'NEWLINE_DELIMITED_JSON' - }; - table.import(file, metadata, callback); - }, - // Here we wait for the job to finish (or fail) in order to log the - // job result, but one could just exit without waiting. - function (job, apiResponse, callback) { - job.on('complete', function () { - console.log('Job complete for ' + data.name); - callback(); - }); - job.on('error', function (err) { - console.error('Job failed for ' + data.name); - callback(err); - }); - } - ], function (err) { - if (err) { - console.error(err); - return context.failure(err); + return Promise.resolve() + .then(() => { + if (!file.bucket) { + throw new Error('Bucket not provided. Make sure you have a "bucket" property in your request'); + } else if (!file.name) { + throw new Error('Filename not provided. Make sure you have a "name" property in your request'); } - return context.success(); + + return getTable(); + }) + .then(([table]) => { + const fileObj = storage.bucket(file.bucket).file(file.name); + console.log(`Starting job for ${file.name}`); + const metadata = { + autodetect: true, + sourceFormat: 'NEWLINE_DELIMITED_JSON' + }; + return table.import(fileObj, metadata); + }) + .then(([job]) => job.promise()) + .then(() => console.log(`Job complete for ${file.name}`)) + .catch((err) => { + console.log(`Job failed for ${file.name}`); + throw err; }); - } catch (err) { - console.error(err); - return context.failure(err.message); - } }; -// [END load] +// [END functions_sendgrid_load] diff --git a/functions/sendgrid/package.json b/functions/sendgrid/package.json index da7fa80629..520b077cf1 100644 --- a/functions/sendgrid/package.json +++ b/functions/sendgrid/package.json @@ -6,15 +6,12 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- functions/sendgrid/test/*.test.js" }, "dependencies": { - "async": "^2.0.1", - "google-cloud": "^0.38.3", + "@google-cloud/storage": "^0.4.0", + "@google-cloud/bigquery": "^0.4.0", "node-uuid": "^1.4.7", - "sendgrid": "^3.0.5" - }, - "devDependencies": { - "mocha": "^3.0.2" + "sendgrid": "^4.7.1" } } diff --git a/functions/sendgrid/test/index.test.js b/functions/sendgrid/test/index.test.js new file mode 100644 index 0000000000..931ba29447 --- /dev/null +++ b/functions/sendgrid/test/index.test.js @@ -0,0 +1,481 @@ +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const proxyquire = require(`proxyquire`).noCallThru(); + +const method = `POST`; +const key = `sengrid_key`; +const to = `receiver@email.com`; +const from = `sender@email.com`; +const subject = `subject`; +const body = `body`; +const auth = `Basic Zm9vOmJhcg==`; +const events = [ + { + sg_message_id: `sendgrid_internal_message_id`, + email: `john.doe@sendgrid.com`, + timestamp: 1337197600, + 'smtp-id': `<4FB4041F.6080505@sendgrid.com>`, + event: `processed` + }, + { + sg_message_id: `sendgrid_internal_message_id`, + email: `john.doe@sendgrid.com`, + timestamp: 1337966815, + category: `newuser`, + event: `click`, + url: `https://sendgrid.com` + }, + { + sg_message_id: `sendgrid_internal_message_id`, + email: `john.doe@sendgrid.com`, + timestamp: 1337969592, + 'smtp-id': `<20120525181309.C1A9B40405B3@Example-Mac.local>`, + event: `group_unsubscribe`, + asm_group_id: 42 + } +]; + +function getSample () { + const config = { + EVENT_BUCKET: 'event-bucket', + DATASET: 'datasets', + TABLE: 'events', + USERNAME: 'foo', + PASSWORD: 'bar' + }; + const request = {}; + const client = { + API: sinon.stub().returns(Promise.resolve({ + statusCode: 200, + body: 'success', + headers: { + 'content-type': 'application/json', + 'content-length': 10 + } + })), + emptyRequest: sinon.stub().returns(request) + }; + const file = { save: sinon.stub().returns(Promise.resolve()) }; + const bucket = { file: sinon.stub().returns(file) }; + const storage = { bucket: sinon.stub().returns(bucket) }; + const job = { promise: sinon.stub().returns(Promise.resolve()) }; + const table = { import: sinon.stub().returns(Promise.resolve([job, {}])) }; + table.get = sinon.stub().returns(Promise.resolve([table])); + const dataset = { table: sinon.stub().returns(table) }; + dataset.get = sinon.stub().returns(Promise.resolve([dataset])); + const bigquery = { dataset: sinon.stub().returns(dataset) }; + const BigQueryMock = sinon.stub().returns(bigquery); + const StorageMock = sinon.stub().returns(storage); + const sendgrid = sinon.stub().returns(client); + const uuid = { v4: sinon.stub() }; + + return { + program: proxyquire(`../`, { + sendgrid: sendgrid, + '@google-cloud/bigquery': BigQueryMock, + '@google-cloud/storage': StorageMock, + './config.json': config, + 'node-uuid': uuid + }), + mocks: { + sendgrid, + client, + request, + bucket, + file, + storage, + bigquery, + dataset, + table, + config, + uuid, + job + } + }; +} + +function getMocks () { + var req = { + headers: {}, + query: {}, + body: {}, + get: function (header) { + return this.headers[header]; + } + }; + sinon.spy(req, 'get'); + var res = { + headers: {}, + send: sinon.stub().returnsThis(), + json: sinon.stub().returnsThis(), + end: sinon.stub().returnsThis(), + status: function (statusCode) { + this.statusCode = statusCode; + return this; + }, + set: function (header, value) { + this.headers[header] = value; + return this; + } + }; + sinon.spy(res, 'status'); + sinon.spy(res, 'set'); + return { + req: req, + res: res + }; +} + +describe(`functions:sendgrid`, () => { + it(`Send fails if not a POST request`, () => { + const error = new Error(`Only POST requests are accepted`); + error.code = 405; + const sample = getSample(); + const mocks = getMocks(); + + return sample.program.sendgridEmail(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [error.code]); + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Send fails without an API key`, () => { + const error = new Error(`SendGrid API key not provided. Make sure you have a "sg_key" property in your request querystring`); + error.code = 401; + const mocks = getMocks(); + + mocks.req.method = method; + + return getSample().program.sendgridEmail(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [error.code]); + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Send fails without a "to"`, () => { + const error = new Error(`To email address not provided. Make sure you have a "to" property in your request`); + error.code = 400; + const mocks = getMocks(); + + mocks.req.method = method; + mocks.req.query.sg_key = key; + return getSample().program.sendgridEmail(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [error.code]); + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Send fails without a "from"`, () => { + const error = new Error(`From email address not provided. Make sure you have a "from" property in your request`); + error.code = 400; + const mocks = getMocks(); + + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + + return getSample().program.sendgridEmail(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [error.code]); + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Send fails without a "subject"`, () => { + const error = new Error(`Email subject line not provided. Make sure you have a "subject" property in your request`); + error.code = 400; + const mocks = getMocks(); + + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + mocks.req.body.from = from; + + return getSample().program.sendgridEmail(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [error.code]); + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Send fails without a "body"`, () => { + const error = new Error(`Email body not provided. Make sure you have a "body" property in your request`); + error.code = 400; + const mocks = getMocks(); + + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + mocks.req.body.from = from; + mocks.req.body.subject = subject; + + return getSample().program.sendgridEmail(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [error.code]); + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Handles response error`, () => { + const mocks = getMocks(); + const sample = getSample(); + + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + mocks.req.body.from = from; + mocks.req.body.subject = subject; + mocks.req.body.body = body; + sample.mocks.client.API.returns(Promise.resolve({ + statusCode: 500, + body: `error` + })); + + return sample.program.sendgridEmail(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [500]); + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args[0].message, `error`); + }); + }); + + it(`Sends the email and successfully responds`, () => { + const mocks = getMocks(); + + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + mocks.req.body.from = from; + mocks.req.body.subject = subject; + mocks.req.body.body = body; + + return getSample().program.sendgridEmail(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [200]); + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [`success`]); + }); + }); + + it(`Handles empty response body`, () => { + const sample = getSample(); + const mocks = getMocks(); + + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + mocks.req.body.from = from; + mocks.req.body.subject = subject; + mocks.req.body.body = body; + + sample.mocks.client.API.returns(Promise.resolve({ + statusCode: 200, + headers: {} + })); + + return sample.program.sendgridEmail(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [200]); + assert.equal(mocks.res.end.callCount, 1); + assert.deepEqual(mocks.res.end.firstCall.args, []); + }); + }); + + it(`Send fails if not a POST request`, () => { + const error = new Error(`Only POST requests are accepted`); + error.code = 405; + const sample = getSample(); + const mocks = getMocks(); + + return sample.program.sendgridWebhook(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Throws if no basic auth`, () => { + const error = new Error(`Invalid credentials`); + error.code = 401; + const sample = getSample(); + const mocks = getMocks(); + + mocks.req.method = method; + mocks.req.headers.authorization = ``; + + return sample.program.sendgridWebhook(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Throws if invalid username`, () => { + const error = new Error(`Invalid credentials`); + error.code = 401; + const sample = getSample(); + const mocks = getMocks(); + + mocks.req.method = method; + mocks.req.headers.authorization = `Basic d3Jvbmc6YmFy`; + + return sample.program.sendgridWebhook(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Throws if invalid password`, () => { + const error = new Error(`Invalid credentials`); + error.code = 401; + const sample = getSample(); + const mocks = getMocks(); + + mocks.req.method = method; + mocks.req.headers.authorization = `Basic Zm9vOndyb25n`; + + return sample.program.sendgridWebhook(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Calls "end" if no events`, () => { + const sample = getSample(); + const mocks = getMocks(); + + mocks.req.method = method; + mocks.req.headers.authorization = auth; + mocks.req.body = undefined; + + return sample.program.sendgridWebhook(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.end.callCount, 1); + assert.deepEqual(mocks.res.end.firstCall.args, []); + }); + }); + + it(`Saves files`, () => { + const sample = getSample(); + const mocks = getMocks(); + + mocks.req.method = `POST`; + mocks.req.headers.authorization = auth; + mocks.req.body = events; + sample.mocks.uuid.v4 = sinon.stub().returns(`1357`); + + return sample.program.sendgridWebhook(mocks.req, mocks.res) + .then(() => { + const filename = sample.mocks.bucket.file.firstCall.args[0]; + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [200]); + assert.equal(mocks.res.end.callCount, 1); + assert.deepEqual(console.log.getCall(0).args, [`Saving events to ${filename} in bucket ${sample.mocks.config.EVENT_BUCKET}`]); + assert.deepEqual(console.log.getCall(1).args, [`JSON written to ${filename}`]); + }); + }); + + it(`sendgridLoad does nothing on delete`, () => { + return getSample().program.sendgridLoad({ + data: { + resourceState: `not_exists` + } + }); + }); + + it(`sendgridLoad fails without a bucket`, () => { + const error = new Error(`Bucket not provided. Make sure you have a "bucket" property in your request`); + const event = { + data: {} + }; + + return getSample().program.sendgridLoad(event) + .then(() => assert.fail(`Should have failed!`)) + .catch((err) => assert.deepEqual(err, error)); + }); + + it(`sendgridLoad fails without a name`, () => { + const error = new Error(`Filename not provided. Make sure you have a "name" property in your request`); + const event = { + data: { + bucket: `event-bucket` + } + }; + + return getSample().program.sendgridLoad(event) + .then(() => assert.fail(`Should have failed!`)) + .catch((err) => assert.deepEqual(err, error)); + }); + + it(`starts a load job`, () => { + const sample = getSample(); + const name = `1234.json`; + const event = { + data: { + bucket: `event-bucket`, + name: name + } + }; + + return sample.program.sendgridLoad(event) + .then(() => { + assert.deepEqual(console.log.getCall(0).args, [`Starting job for ${name}`]); + assert.deepEqual(console.log.getCall(1).args, [`Job complete for ${name}`]); + }); + }); +}); diff --git a/functions/slack/.gitignore b/functions/slack/.gitignore index 254077b7f3..11dc9156b6 100644 --- a/functions/slack/.gitignore +++ b/functions/slack/.gitignore @@ -1,4 +1,3 @@ node_modules config.json test.js -test/ \ No newline at end of file diff --git a/functions/slack/index.js b/functions/slack/index.js index e07217dd75..2a86e56a93 100644 --- a/functions/slack/index.js +++ b/functions/slack/index.js @@ -1,59 +1,60 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -// [START setup] -var config = require('./config.json'); -var googleapis = require('googleapis'); +// [START functions_slack_setup] +const config = require('./config.json'); +const googleapis = require('googleapis'); // Get a reference to the Knowledge Graph Search component -var kgsearch = googleapis.kgsearch('v1'); -// [END setup] +const kgsearch = googleapis.kgsearch('v1'); +// [END functions_slack_setup] -// [START formatSlackMessage] +// [START functions_slack_format] /** * Format the Knowledge Graph API response into a richly formatted Slack message. * * @param {string} query The user's search query. - * @param {Object} response The response from the Knowledge Graph API. - * @returns {Object} The formatted message. + * @param {object} response The response from the Knowledge Graph API. + * @returns {object} The formatted message. */ function formatSlackMessage (query, response) { - var entity; + let entity; // Extract the first entity from the result list, if any - if (response && response.itemListElement && - response.itemListElement.length) { + if (response && response.itemListElement && response.itemListElement.length > 0) { entity = response.itemListElement[0].result; } // Prepare a rich Slack message // See https://api.slack.com/docs/message-formatting - var slackMessage = { + const slackMessage = { response_type: 'in_channel', - text: 'Query: ' + query, + text: `Query: ${query}`, attachments: [] }; if (entity) { - var attachment = { + const attachment = { color: '#3367d6' }; if (entity.name) { attachment.title = entity.name; if (entity.description) { - attachment.title = attachment.title + ': ' + entity.description; + attachment.title = `${attachment.title}: ${entity.description}`; } } if (entity.detailedDescription) { @@ -76,48 +77,51 @@ function formatSlackMessage (query, response) { return slackMessage; } -// [END formatSlackMessage] +// [END functions_slack_format] -// [START verifyWebhook] +// [START functions_verify_webhook] /** * Verify that the webhook request came from Slack. * - * @param {Object} body The body of the request. + * @param {object} body The body of the request. * @param {string} body.token The Slack token to be verified. */ function verifyWebhook (body) { if (!body || body.token !== config.SLACK_TOKEN) { - var error = new Error('Invalid credentials'); + const error = new Error('Invalid credentials'); error.code = 401; throw error; } } -// [END verifyWebhook] +// [END functions_verify_webhook] -// [START makeSearchRequest] +// [START functions_slack_request] /** * Send the user's search query to the Knowledge Graph API. * * @param {string} query The user's search query. - * @param {Function} callback Callback function. */ -function makeSearchRequest (query, callback) { - kgsearch.entities.search({ - auth: config.KG_API_KEY, - query: query, - limit: 1 - }, function (err, response) { - if (err) { - return callback(err); - } +function makeSearchRequest (query) { + return new Promise((resolve, reject) => { + kgsearch.entities.search({ + auth: config.KG_API_KEY, + query: query, + limit: 1 + }, (err, response) => { + console.log(err); + if (err) { + reject(err); + return; + } - // Return a formatted message - return callback(null, formatSlackMessage(query, response)); + // Return a formatted message + resolve(formatSlackMessage(query, response)); + }); }); } -// [END makeSearchRequest] +// [END functions_slack_request] -// [START kgSearch] +// [START functions_slack_search] /** * Receive a Slash Command request from Slack. * @@ -127,36 +131,34 @@ function makeSearchRequest (query, callback) { * @example * curl -X POST "https://us-central1.your-project-id.cloudfunctions.net/kgSearch" --data '{"token":"[YOUR_SLACK_TOKEN]","text":"giraffe"}' * - * @param {Object} req Cloud Function request object. - * @param {Object} req.body The request payload. + * @param {object} req Cloud Function request object. + * @param {object} req.body The request payload. * @param {string} req.body.token Slack's verification token. * @param {string} req.body.text The user's search query. - * @param {Object} res Cloud Function response object. + * @param {object} res Cloud Function response object. */ exports.kgSearch = function kgSearch (req, res) { - try { - if (req.method !== 'POST') { - var error = new Error('Only POST requests are accepted'); - error.code = 405; - throw error; - } - - // Verify that this request came from Slack - verifyWebhook(req.body); - - // Make the request to the Knowledge Graph Search API - makeSearchRequest(req.body.text, function (err, response) { - if (err) { - console.error(err); - return res.status(500); + return Promise.resolve() + .then(() => { + if (req.method !== 'POST') { + const error = new Error('Only POST requests are accepted'); + error.code = 405; + throw error; } + // Verify that this request came from Slack + verifyWebhook(req.body); + + // Make the request to the Knowledge Graph Search API + return makeSearchRequest(req.body.text); + }) + .then((response) => { // Send the formatted message back to Slack - return res.json(response); + res.json(response); + }) + .catch((err) => { + console.error(err); + res.status(err.code || 500).send(err); }); - } catch (err) { - console.error(err); - return res.status(err.code || 500).send(err.message); - } }; -// [END kgSearch] +// [END functions_slack_search] diff --git a/functions/slack/package.json b/functions/slack/package.json index 153814296d..6e72da5ee1 100644 --- a/functions/slack/package.json +++ b/functions/slack/package.json @@ -6,12 +6,9 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- functions/slack/test/*.test.js" }, "dependencies": { - "googleapis": "^12.0.0" - }, - "devDependencies": { - "mocha": "^2.5.3" + "googleapis": "^14.1.0" } } diff --git a/functions/slack/test/index.test.js b/functions/slack/test/index.test.js new file mode 100644 index 0000000000..a011955426 --- /dev/null +++ b/functions/slack/test/index.test.js @@ -0,0 +1,244 @@ +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const proxyquire = require(`proxyquire`).noCallThru(); + +const method = `POST`; +const query = `giraffe`; +const SLACK_TOKEN = `slack-token`; +const KG_API_KEY = `kg-api-key`; + +function getSample () { + const config = { + SLACK_TOKEN: SLACK_TOKEN, + KG_API_KEY: KG_API_KEY + }; + const kgsearch = { + entities: { + search: sinon.stub().yields() + } + }; + const googleapis = { + kgsearch: sinon.stub().returns(kgsearch) + }; + + return { + program: proxyquire(`../`, { + googleapis: googleapis, + './config.json': config + }), + mocks: { + googleapis: googleapis, + kgsearch: kgsearch, + config: config + } + }; +} + +function getMocks () { + const req = { + headers: {}, + query: {}, + body: {}, + get: function (header) { + return this.headers[header]; + } + }; + sinon.spy(req, 'get'); + const res = { + headers: {}, + send: sinon.stub().returnsThis(), + json: sinon.stub().returnsThis(), + end: sinon.stub().returnsThis(), + status: function (statusCode) { + this.statusCode = statusCode; + return this; + }, + set: function (header, value) { + this.headers[header] = value; + return this; + } + }; + sinon.spy(res, 'status'); + sinon.spy(res, 'set'); + return { + req: req, + res: res + }; +} + +describe(`functions:slack`, () => { + it(`Send fails if not a POST request`, () => { + const error = new Error(`Only POST requests are accepted`); + error.code = 405; + const mocks = getMocks(); + const sample = getSample(); + + return sample.program.kgSearch(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [error.code]); + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Throws if invalid slack token`, () => { + const error = new Error(`Invalid credentials`); + error.code = 401; + const mocks = getMocks(); + const sample = getSample(); + + mocks.req.method = method; + mocks.req.body.token = 'wrong'; + return sample.program.kgSearch(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [error.code]); + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Handles search error`, () => { + const error = new Error(`error`); + const mocks = getMocks(); + const sample = getSample(); + + mocks.req.method = method; + mocks.req.body.token = SLACK_TOKEN; + mocks.req.body.text = query; + sample.mocks.kgsearch.entities.search.yields(error); + + return sample.program.kgSearch(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.status.callCount, 1); + assert.deepEqual(mocks.res.status.firstCall.args, [500]); + assert.equal(mocks.res.send.callCount, 1); + assert.deepEqual(mocks.res.send.firstCall.args, [error]); + assert.equal(console.error.callCount, 1); + assert.deepEqual(console.error.firstCall.args, [error]); + }); + }); + + it(`Makes search request, receives empty results`, () => { + const mocks = getMocks(); + const sample = getSample(); + + mocks.req.method = method; + mocks.req.body.token = SLACK_TOKEN; + mocks.req.body.text = query; + sample.mocks.kgsearch.entities.search.yields(null, { itemListElement: [] }); + + return sample.program.kgSearch(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.json.callCount, 1); + assert.deepEqual(mocks.res.json.firstCall.args, [{ + text: `Query: ${query}`, + response_type: `in_channel`, + attachments: [ + { + text: `No results match your query...` + } + ] + }]); + }); + }); + + it(`Makes search request, receives non-empty results`, () => { + const mocks = getMocks(); + const sample = getSample(); + + mocks.req.method = method; + mocks.req.body.token = SLACK_TOKEN; + mocks.req.body.text = query; + sample.mocks.kgsearch.entities.search.yields(null, { + itemListElement: [ + { + result: { + name: `Giraffe`, + description: `Animal`, + detailedDescription: { + url: `http://domain.com/giraffe`, + articleBody: `giraffe is a tall animal` + }, + image: { + contentUrl: `http://domain.com/image.jpg` + } + } + } + ] + }); + + return sample.program.kgSearch(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.json.callCount, 1); + assert.deepEqual(mocks.res.json.firstCall.args, [{ + text: `Query: ${query}`, + response_type: `in_channel`, + attachments: [ + { + color: `#3367d6`, + title: `Giraffe: Animal`, + title_link: `http://domain.com/giraffe`, + text: `giraffe is a tall animal`, + image_url: `http://domain.com/image.jpg` + } + ] + }]); + }); + }); + + it(`Makes search request, receives non-empty results but partial data`, () => { + const mocks = getMocks(); + const sample = getSample(); + + mocks.req.method = method; + mocks.req.body.token = SLACK_TOKEN; + mocks.req.body.text = query; + sample.mocks.kgsearch.entities.search.yields(null, { + itemListElement: [ + { + result: { + name: `Giraffe`, + detailedDescription: {}, + image: {} + } + } + ] + }); + + return sample.program.kgSearch(mocks.req, mocks.res) + .then(() => { + assert.equal(mocks.res.json.callCount, 1); + assert.deepEqual(mocks.res.json.firstCall.args, [{ + text: `Query: ${query}`, + response_type: `in_channel`, + attachments: [ + { + color: `#3367d6`, + title: `Giraffe` + } + ] + }]); + }); + }); +}); diff --git a/functions/uuid/index.js b/functions/uuid/index.js index a1987443a9..3d3c5d41c6 100644 --- a/functions/uuid/index.js +++ b/functions/uuid/index.js @@ -1,22 +1,24 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -// [START uuid] -var uuid = require('node-uuid'); +// [START functions_uuid] +const uuid = require('node-uuid'); -exports.uuid = function (context, data) { - context.success(uuid.v4()); +exports.uuid = function (event, callback) { + callback(null, uuid.v4()); }; -// [END uuid] +// [END functions_uuid] diff --git a/functions/uuid/package.json b/functions/uuid/package.json index d94bc99d9d..c028f7b95b 100644 --- a/functions/uuid/package.json +++ b/functions/uuid/package.json @@ -6,12 +6,9 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- functions/uuid/test/*.test.js" }, "dependencies": { "node-uuid": "^1.4.7" - }, - "devDependencies": { - "mocha": "^2.5.3" } } diff --git a/functions/uuid/test/index.test.js b/functions/uuid/test/index.test.js index 6d14f0cb93..6079441d80 100644 --- a/functions/uuid/test/index.test.js +++ b/functions/uuid/test/index.test.js @@ -1,28 +1,31 @@ -// Copyright 2016, Google, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ 'use strict'; -var uuidSample = require('../'); +const uuidSample = require('../'); -describe('functions:uuid', function () { - it('should generate a uuid', function (done) { - uuidSample.uuid({ - success: function (uuid) { - assert.equal(typeof uuid, 'string'); - assert.equal(uuid.length, 36); - done(); - } - }); +describe(`functions:uuid`, () => { + it(`should generate a uuid`, () => { + const callback = sinon.stub(); + + uuidSample.uuid({}, callback); + + assert.equal(callback.callCount, 1); + assert.strictEqual(callback.firstCall.args[0], null); + assert.equal(typeof callback.firstCall.args[1], `string`); + assert.equal(callback.firstCall.args[1].length, 36); }); }); diff --git a/logging/logs.js b/logging/logs.js index ce15b14760..120694d058 100644 --- a/logging/logs.js +++ b/logging/logs.js @@ -33,10 +33,10 @@ function writeLogEntry (logName, callback) { }; // A text log entry - var entry = log.entry(resource, 'Hello, world!'); + var entry = log.entry({ resource: resource }, 'Hello, world!'); // A structured log entry - var secondEntry = log.entry(resource, { + var secondEntry = log.entry({ resource: resource }, { name: 'King Arthur', quest: 'Find the Holy Grail', favorite_color: 'Blue' @@ -62,7 +62,7 @@ function writeLogEntryAdvanced (logName, options, callback) { var log = logging.log(logName); // Prepare the entry - var entry = log.entry(options.resource, options.entry); + var entry = log.entry({ resource: options.resource }, options.entry); // See https://googlecloudplatform.github.io/google-cloud-node/#/docs/logging/latest/logging/log?method=write log.write(entry, function (err, apiResponse) { diff --git a/logging/package.json b/logging/package.json index 83b5de5b0e..abb9cd9150 100644 --- a/logging/package.json +++ b/logging/package.json @@ -5,19 +5,18 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../test/_setup.js test/*.test.js", - "system-test": "mocha -R spec -t 120000 --require intelli-espower-loader ../system-test/_setup.js system-test/*.test.js" + "test": "cd ..; npm run t -- logging/test/*.test.js", + "system-test": "cd ..; npm run st -- logging/system-test/*.test.js" }, "dependencies": { - "@google-cloud/logging": "^0.1.1", - "@google-cloud/storage": "^0.1.1", - "express": "^4.13.4", - "fluent-logger": "^2.0.1", - "yargs": "^5.0.0" + "@google-cloud/logging": "0.5.0", + "@google-cloud/storage": "0.4.0", + "express": "4.14.0", + "fluent-logger": "2.0.1", + "yargs": "6.4.0" }, "devDependencies": { - "mocha": "^3.0.2", - "node-uuid": "^1.4.7" + "node-uuid": "1.4.7" }, "engines": { "node": ">=4.3.2" diff --git a/logging/quickstart.js b/logging/quickstart.js index 84a1ab5f52..c87353a607 100644 --- a/logging/quickstart.js +++ b/logging/quickstart.js @@ -34,10 +34,10 @@ const log = loggingClient.log(logName); // The data to write to the log const text = 'Hello, world!'; -// The resource associated with the data -const resource = { type: 'global' }; +// The metadata associated with the entry +const metadata = { resource: { type: 'global' } }; // Prepares a log entry -const entry = log.entry(resource, text); +const entry = log.entry(metadata, text); // Writes the log entry log.write(entry, (err) => { diff --git a/logging/system-test/sinks.test.js b/logging/system-test/sinks.test.js index f62f449f38..5a55074bf0 100644 --- a/logging/system-test/sinks.test.js +++ b/logging/system-test/sinks.test.js @@ -58,7 +58,8 @@ describe('logging:sinks', function () { name: sinkName, destination: 'storage.googleapis.com/' + bucketName, filter: filter, - outputVersionFormat: 'V2' + outputVersionFormat: 'V2', + writerIdentity: 'serviceAccount:cloud-logs@system.gserviceaccount.com' }; program.getSinkMetadata(sinkName, function (err, metadata) { @@ -90,7 +91,8 @@ describe('logging:sinks', function () { name: sinkName, destination: 'storage.googleapis.com/' + bucketName, filter: newFilter, - outputVersionFormat: 'V2' + outputVersionFormat: 'V2', + writerIdentity: 'serviceAccount:cloud-logs@system.gserviceaccount.com' }; program.updateSink(sinkName, newFilter, function (err, apiResponse) { diff --git a/logging/test/quickstart.test.js b/logging/test/quickstart.test.js index 2e9a8124f3..a087300303 100644 --- a/logging/test/quickstart.test.js +++ b/logging/test/quickstart.test.js @@ -21,7 +21,7 @@ describe(`logging:quickstart`, () => { let logMock, loggingMock, LoggingMock; const error = new Error(`error`); const expectedLogName = `my-log`; - const expectedResource = { type: `global` }; + const expectedResource = { resource: { type: `global` } }; const expectedMessage = `Hello, world!`; before(() => { diff --git a/monitoring/package.json b/monitoring/package.json index 046f99a395..23f7396580 100644 --- a/monitoring/package.json +++ b/monitoring/package.json @@ -5,14 +5,11 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../test/_setup.js test/*.test.js", - "system-test": "mocha -R spec -t 120000 --require intelli-espower-loader ../system-test/_setup.js system-test/*.test.js" + "test": "cd ..; npm run t -- monitoring/test/*.test.js", + "system-test": "cd ..; npm run st -- monitoring/system-test/*.test.js" }, "dependencies": { - "async":"^1.5.2", - "googleapis": "^12.0.0" - }, - "devDependencies": { - "mocha": "^2.5.3" + "async":"2.1.2", + "googleapis": "14.2.0" } } diff --git a/package.json b/package.json index 5e75cabfe9..85a136de07 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "pretest": "npm run lint && ./scripts/clean", "t": "mocha -R spec -t 2000 --require intelli-espower-loader test/_setup.js", "st": "mocha -R spec -t 120000 --require intelli-espower-loader system-test/_setup.js", - "mocha": "npm run t -- ./test/_setup.js ./test/*.test.js '{*,appengine/*,functions/*}/test/*.test.js'", + "mocha": "npm run t -- ./test/_setup.js ./test/*.test.js '{*,appengine/*,functions/*,functions/ocr/*}/test/*.test.js'", "test": "npm run mocha", "cover": "nyc --cache npm test && nyc report --reporter=html && nyc report --reporter=lcov", "system-test": "mocha -R spec -t 120000 --require intelli-espower-loader ./system-test/_setup.js '{*,appengine/*}/system-test/*.test.js'", @@ -65,17 +65,17 @@ "all-cover": "npm run pretest && nyc --cache npm run all-test && nyc report --reporter=html && nyc report --reporter=lcov" }, "devDependencies": { - "async": "^2.0.1", - "intelli-espower-loader": "^1.0.1", - "mocha": "^3.0.2", + "async": "2.1.2", + "intelli-espower-loader": "1.0.1", + "mocha": "3.1.2", "nodejs-repo-tools": "git+https://github.com/GoogleCloudPlatform/nodejs-repo-tools.git#bbbb6035d77671eb053dbe6b6f0e3ff983f79639", - "nyc": "^8.1.0", - "power-assert": "^1.4.1", - "proxyquire": "^1.7.10", - "request": "^2.72.0", - "semistandard": "^9.0.0", - "shelljs": "^0.7.3", - "sinon": "^1.17.5", - "supertest": "^2.0.0" + "nyc": "9.0.1", + "power-assert": "1.4.2", + "proxyquire": "1.7.10", + "request": "2.78.0", + "semistandard": "9.1.0", + "shelljs": "0.7.5", + "sinon": "1.17.6", + "supertest": "2.0.1" } } diff --git a/prediction/package.json b/prediction/package.json index 3c4421e015..f622a58395 100644 --- a/prediction/package.json +++ b/prediction/package.json @@ -5,13 +5,10 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../test/_setup.js test/*.test.js", - "system-test": "mocha -R spec -t 120000 --require intelli-espower-loader ../system-test/_setup.js system-test/*.test.js" + "test": "cd ..; npm run t -- prediction/test/*.test.js", + "system-test": "cd ..; npm run st -- prediction/system-test/*.test.js" }, "dependencies": { - "googleapis": "^12.0.0" - }, - "devDependencies": { - "mocha": "^2.5.3" + "googleapis": "14.2.0" } } diff --git a/resource/package.json b/resource/package.json index e067a12bbd..52bc552346 100644 --- a/resource/package.json +++ b/resource/package.json @@ -5,15 +5,12 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../test/_setup.js test/*.test.js", - "system-test": "mocha -R spec -t 120000 --require intelli-espower-loader ../system-test/_setup.js system-test/*.test.js" + "test": "cd ..; npm run t -- resource/test/*.test.js", + "system-test": "cd ..; npm run st -- resource/system-test/*.test.js" }, "dependencies": { - "@google-cloud/resource": "^0.2.0", - "yargs": "^6.0.0" - }, - "devDependencies": { - "mocha": "^3.1.0" + "@google-cloud/resource": "0.3.0", + "yargs": "6.4.0" }, "engines": { "node": ">=4.3.2" diff --git a/scripts/updates b/scripts/updates new file mode 100644 index 0000000000..7957c9f092 --- /dev/null +++ b/scripts/updates @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +// Copyright 2016, Google, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +var async = require('async'); +var path = require('path'); + +require('shelljs/global'); + +// Check NPM dependencies, in up to 7 directories at a time +var queue = async.queue(function (directory, callback) { + checkForDirectory(directory, callback); +}, 1); + +queueDirectories('appengine'); +queue.push('bigquery'); +queue.push('computeengine'); +queue.push('datastore'); +queue.push('debugger'); +queue.push('dns'); +queueDirectories('functions'); +queue.push('functions/ocr/app'); +queue.push('language'); +queue.push('logging'); +queue.push('monitoring'); +queue.push('prediction'); +queue.push('pubsub'); +queue.push('resource'); +queue.push('speech'); +queue.push('storage'); +queue.push('trace'); +queue.push('translate'); +queue.push('vision'); + +/** + * Check NPM dependencies within a single directory. + * + * @param {string} directory The name of the directory in which to check dependencies. + * @param {function} callback The callback function. + */ +function checkForDirectory(directory, callback) { + console.log(directory + '...checking dependencies'); + exec('npm outdated', { + async: true, + cwd: path.join(__dirname, '../', directory) + }, callback); +} + +/** + * Recursively check NPM dependencies within a single directory. + * + * @param {string} directory The name of the directory in which to recursively check dependencies. + */ +function queueDirectories(directory) { + // Move into the directory + cd(directory); + + // List the files in the directory + ls('-dl', '*') + .filter(function (file) { + // Find the directories within the directory + return file.isDirectory() && file.name !== 'test' && file.name !== 'system-test'; + }) + .forEach(function (file) { + queue.push(directory + '/' + file.name); + }); + + // Move out of the directory + cd('..'); +} \ No newline at end of file diff --git a/storage/package.json b/storage/package.json index c4588c6195..41fb6a577a 100644 --- a/storage/package.json +++ b/storage/package.json @@ -5,18 +5,18 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "scripts": { - "test": "mocha -R spec -t 120000 ../test/_setup.js test/*.test.js", - "system-test": "mocha -R spec -t 120000 ../system-test/_setup.js system-test/*.test.js" + "test": "cd ..; npm run t -- storage/test/*.test.js", + "system-test": "cd ..; npm run st -- storage/system-test/*.test.js" }, "dependencies": { - "@google-cloud/storage": "^0.2.0", - "googleapis": "^13.0.0", - "moment": "^2.15.1", - "yargs": "^6.0.0" + "@google-cloud/storage": "0.4.0", + "googleapis": "14.2.0", + "moment": "2.15.1", + "yargs": "6.4.0" }, "devDependencies": { - "mocha": "^3.1.0", - "node-uuid": "^1.4.7" + "mocha": "3.1.0", + "node-uuid": "1.4.7" }, "engines": { "node": ">=4.3.2" diff --git a/trace/package.json b/trace/package.json index 54d255b7a9..c9f3b34a73 100644 --- a/trace/package.json +++ b/trace/package.json @@ -9,14 +9,11 @@ }, "scripts": { "start": "node app.js", - "test": "mocha -R spec --require intelli-espower-loader ../test/_setup.js test/*.test.js" + "test": "cd ..; npm run t -- trace/test/*.test.js", }, "dependencies": { - "@google/cloud-trace": "^0.5.10", - "express": "^4.14.0", - "request": "^2.78.0" - }, - "devDependencies": { - "mocha": "^3.1.2" + "@google/cloud-trace": "0.5.10", + "express": "4.14.0", + "request": "2.78.0" } } diff --git a/vision/package.json b/vision/package.json index 4c01a68cf7..313d4eb283 100644 --- a/vision/package.json +++ b/vision/package.json @@ -5,20 +5,17 @@ "license": "Apache Version 2.0", "author": "Google Inc.", "scripts": { - "test": "mocha -R spec -t 10000 --require intelli-espower-loader ../test/_setup.js test/*.test.js", - "system-test": "mocha -R spec -t 10000 --require intelli-espower-loader ../system-test/_setup.js system-test/*.test.js" + "test": "cd ..; npm run t -- vision/test/*.test.js", + "system-test": "cd ..; npm run st -- vision/system-test/*.test.js" }, "dependencies": { - "@google-cloud/vision": "^0.5.0", - "async": "^2.1.2", - "natural": "^0.4.0", - "redis": "^2.6.3" - }, - "devDependencies": { - "mocha": "^3.1.2" + "@google-cloud/vision": "0.5.0", + "async": "2.1.2", + "natural": "0.4.0", + "redis": "2.6.3" }, "optionalDependencies": { - "canvas": "^1.6.2" + "canvas": "1.6.2" }, "engines": { "node": ">=4.3.2"