From 8cb8f04e52c567d45bd41862e7020ab30b313848 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Thu, 6 Oct 2016 09:58:35 -0700 Subject: [PATCH] Refactor Cloud Functions samples. --- functions/background/index.js | 78 +-- functions/background/package.json | 5 +- functions/background/test/index.test.js | 133 ++--- functions/datastore/README.md | 70 ++- functions/datastore/index.js | 181 +++---- functions/datastore/package.json | 4 +- functions/datastore/test/index.test.js | 474 ++++++++--------- functions/errorreporting/index.js | 128 ++--- functions/errorreporting/package.json | 4 +- functions/errorreporting/report.js | 83 +-- functions/errorreporting/test/index.test.js | 30 +- functions/gcs/README.md | 16 +- functions/gcs/index.js | 105 ++-- functions/gcs/package.json | 5 +- functions/gcs/test/index.test.js | 141 +++-- functions/helloworld/index.js | 145 ++++-- functions/helloworld/package.json | 5 +- functions/helloworld/test/index.test.js | 232 +++++---- functions/http/index.js | 106 ++-- functions/http/package.json | 2 +- functions/http/test/index.test.js | 146 +++--- functions/log/index.js | 162 +++--- functions/log/package.json | 6 +- functions/log/test/index.test.js | 147 +++--- functions/ocr/app/index.js | 351 ++++++------- functions/ocr/app/package.json | 11 +- functions/ocr/app/test/index.test.js | 537 +++++++------------- functions/pubsub/README.md | 22 +- functions/pubsub/index.js | 125 ++--- functions/pubsub/package.json | 4 +- functions/pubsub/test/index.test.js | 206 ++++---- functions/sendgrid/index.js | 403 +++++++-------- functions/sendgrid/package.json | 10 +- functions/slack/index.js | 144 +++--- functions/slack/package.json | 6 +- functions/uuid/index.js | 36 +- functions/uuid/package.json | 4 +- functions/uuid/test/index.test.js | 47 +- 38 files changed, 2072 insertions(+), 2242 deletions(-) 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..30cc6d5f58 100644 --- a/functions/background/package.json +++ b/functions/background/package.json @@ -9,9 +9,10 @@ "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" }, "dependencies": { - "request-promise": "^3.0.0" + "request": "^2.75.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..58f84b3fd9 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,75 @@ 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. 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. + * Replace `YOUR_BUCKET_NAME` with the name of your Cloud Storage Bucket. -1. Deploy the "ds-set" function with an HTTP trigger: +1. Deploy the "set" function with an HTTP trigger: - gcloud alpha functions deploy ds-set --bucket [YOUR_BUCKET_NAME] --trigger-http --entry-point set + gcloud alpha functions deploy set --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 "ds-del" function with an HTTP trigger: +1. Deploy the "del" function with an HTTP trigger: - gcloud alpha functions deploy ds-del --bucket [YOUR_BUCKET_NAME] --trigger-http --entry-point del + gcloud alpha functions deploy del --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 "ds-set" function to create a new entity: +1. Call the "set" function to create a new entity: - 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"}}' -1. Call the "ds-get" function to read the newly created entity: + or - gcloud alpha functions call ds-get --data '{"kind":"gcf-test","key":"foobar"}' + 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" -1. Call the "ds-del" function to delete the entity: + * 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-del --data '{"kind":"gcf-test","key":"foobar"}' +1. Call the "get" function to read the newly created entity: -1. Call the "ds-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. + +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..ceeb875619 100644 --- a/functions/datastore/package.json +++ b/functions/datastore/package.json @@ -9,9 +9,9 @@ "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" }, "dependencies": { - "@google-cloud/datastore": "^0.1.1" + "@google-cloud/datastore": "^0.5.0" }, "devDependencies": { - "mocha": "^3.0.2" + "mocha": "^3.1.2" } } 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..6f6f1ac117 100644 --- a/functions/errorreporting/package.json +++ b/functions/errorreporting/package.json @@ -9,9 +9,9 @@ "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" }, "dependencies": { - "@google-cloud/logging": "^0.1.1" + "@google-cloud/logging": "^0.5.0" }, "devDependencies": { - "mocha": "^3.0.2" + "mocha": "^3.1.2" } } 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..668f883555 100644 --- a/functions/gcs/index.js +++ b/functions/gcs/index.js @@ -1,70 +1,75 @@ -// 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'); +// [START functions_word_count_stream] +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(); } +// [END functions_word_count_stream] +// [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..c53812a4a6 100644 --- a/functions/gcs/package.json +++ b/functions/gcs/package.json @@ -9,9 +9,10 @@ "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" }, "dependencies": { - "@google-cloud/storage": "^0.1.1" + "@google-cloud/storage": "^0.4.0", + "request": "^2.75.0" }, "devDependencies": { - "mocha": "^3.0.2" + "mocha": "^3.1.2" } } diff --git a/functions/gcs/test/index.test.js b/functions/gcs/test/index.test.js index c05b9e2fb2..c0397e5ea9 100644 --- a/functions/gcs/test/index.test.js +++ b/functions/gcs/test/index.test.js @@ -1,38 +1,39 @@ -// 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 file = { + createReadStream: () => fs.createReadStream(path.join(__dirname, `../${filename}`), { 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 +45,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..959f1b7e80 100644 --- a/functions/helloworld/package.json +++ b/functions/helloworld/package.json @@ -9,6 +9,9 @@ "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" }, "devDependencies": { - "mocha": "^2.5.3" + "mocha": "^3.1.2" + }, + "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..b5d808f985 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 an error`, () => { + assert.throws(() => { + program.helloError2(); + }); + }); + + it(`helloError3: should throw an error`, () => { + 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..71a2362aa3 100644 --- a/functions/http/package.json +++ b/functions/http/package.json @@ -9,6 +9,6 @@ "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" }, "devDependencies": { - "mocha": "^2.5.3" + "mocha": "^3.1.2" } } 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..295a29d6fc 100644 --- a/functions/log/package.json +++ b/functions/log/package.json @@ -9,10 +9,10 @@ "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" }, "dependencies": { - "@google-cloud/logging": "^0.1.1", - "googleapis": "^12.2.0" + "@google-cloud/logging": "^0.5.0", + "@google-cloud/monitoring": "^0.1.0" }, "devDependencies": { - "mocha": "^3.0.2" + "mocha": "^3.1.2" } } 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/index.js b/functions/ocr/app/index.js index 6d278cecf4..ed52e9ec7d 100644 --- a/functions/ocr/app/index.js +++ b/functions/ocr/app/index.js @@ -1,105 +1,90 @@ -// 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')({ +const translate = require('@google-cloud/translate')({ key: config.TRANSLATE_API_KEY }); -// [END ocr_setup] +// [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]) => { + text = _text; + console.log(`Extracted text from image (${text.length} chars)`); + return translate.detect(text); + }) + .then(([translation]) => { + console.log(`Detected language "${translation.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 (translation.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: translation.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 +93,140 @@ function detectText (filename, image, callback) { * @returns {string} The new filename. */ function renameImageForSave (filename, lang) { - var dotIndex = filename.indexOf('.'); - var suffix = '_to_' + lang + '.txt'; + const dotIndex = filename.indexOf('.'); + const suffix = `_to_${lang}.txt`; if (dotIndex !== -1) { filename = filename.replace(/\.[^/.]+$/, suffix); } else { - filename += suffix; + filename = `${filename}${suffix}`; } return filename; } -// [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'); } - console.log('Processed ' + data.name); - return context.success(); + if (!file.name) { + throw new Error('Filename not provided. Make sure you have a "name" property in your request'); + } + + 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'); } - return publishResult(config.RESULT_TOPIC, { + const options = { + from: payload.from, + to: payload.lang + }; + + 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..a0574dc631 100644 --- a/functions/ocr/app/package.json +++ b/functions/ocr/app/package.json @@ -9,13 +9,12 @@ "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../../test/_setup.js 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" + "@google-cloud/pubsub": "^0.5.0", + "@google-cloud/storage": "^0.4.0", + "@google-cloud/translate": "^0.4.0", + "@google-cloud/vision": "^0.5.0" }, "devDependencies": { - "mocha": "^3.0.2" + "mocha": "^3.1.2" } } diff --git a/functions/ocr/app/test/index.test.js b/functions/ocr/app/test/index.test.js index 776183bc0e..adf7f1c22d 100644 --- a/functions/ocr/app/test/index.test.js +++ b/functions/ocr/app/test/index.test.js @@ -1,65 +1,70 @@ -// 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 savedFileName = `image_to_${lang}.txt`; +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 = { + TRANSLATE_API_KEY: `key`, + 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 +72,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: 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: 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: 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: 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 ${savedFileName} 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}_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..cc3d8c068b 100644 --- a/functions/pubsub/package.json +++ b/functions/pubsub/package.json @@ -9,9 +9,9 @@ "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" }, "dependencies": { - "@google-cloud/pubsub": "^0.1.1" + "@google-cloud/pubsub": "^0.5.0" }, "devDependencies": { - "mocha": "^3.0.2" + "mocha": "^3.1.2" } } diff --git a/functions/pubsub/test/index.test.js b/functions/pubsub/test/index.test.js index aaea5a0370..d25a568504 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.deepEqual(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.deepEqual(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/index.js b/functions/sendgrid/index.js index aa514506c6..3bd1051ac0 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,129 @@ 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 {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 {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 */ -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..45a9f719bc 100644 --- a/functions/sendgrid/package.json +++ b/functions/sendgrid/package.json @@ -6,15 +6,15 @@ "author": "Google Inc.", "main": "./index.js", "scripts": { - "test": "mocha -R spec -t 120000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" + "test": "mocha -R spec -t 1000 --require intelli-espower-loader ../../test/_setup.js 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" + "sendgrid": "^4.7.1" }, "devDependencies": { - "mocha": "^3.0.2" + "mocha": "^3.1.2" } } diff --git a/functions/slack/index.js b/functions/slack/index.js index e07217dd75..b8473fb7d5 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) { 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..455bec08fc 100644 --- a/functions/slack/package.json +++ b/functions/slack/package.json @@ -6,12 +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": "mocha -R spec -t 1000 --require intelli-espower-loader ../../test/_setup.js test/*.test.js" }, "dependencies": { - "googleapis": "^12.0.0" + "googleapis": "^14.1.0" }, "devDependencies": { - "mocha": "^2.5.3" + "mocha": "^3.1.2" } } 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..5eb03d79b1 100644 --- a/functions/uuid/package.json +++ b/functions/uuid/package.json @@ -6,12 +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": "mocha -R spec --require intelli-espower-loader ../../test/_setup.js test/*.test.js" }, "dependencies": { "node-uuid": "^1.4.7" }, "devDependencies": { - "mocha": "^2.5.3" + "mocha": "^3.1.2" } } 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); }); });