From 4a1c20de1dd5131280b10e3c5195b10aa37ae5b0 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Wed, 15 Jun 2016 11:11:18 -0700 Subject: [PATCH 01/23] Add HTTP Cloud Function. --- functions/README.md | 3 ++- functions/http/README.md | 35 ++++++++++++++++++++++++++++++++ functions/http/index.js | 43 ++++++++++++++++++++++++++++++++++++++++ functions/log2/index.js | 6 +++--- 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 functions/http/README.md create mode 100644 functions/http/index.js diff --git a/functions/README.md b/functions/README.md index 02f526e4a8..f604a8b184 100644 --- a/functions/README.md +++ b/functions/README.md @@ -24,6 +24,7 @@ environment. * [Cloud Storage](gcs/) * [Cloud Datastore](datastore/) * [Cloud Pub/Sub](pubsub/) -* [Dependencies](uid/) +* [Dependencies](uuid/) +* [HTTP](http/) * [Logging](log/) * [Modules](module/) diff --git a/functions/http/README.md b/functions/http/README.md new file mode 100644 index 0000000000..fc2f61eb91 --- /dev/null +++ b/functions/http/README.md @@ -0,0 +1,35 @@ +Google Cloud Platform logo + +# Google Cloud Functions HTTP sample + +This recipe shows you how to respond to HTTP requests with a Cloud Function. + +View the [source code][code]. + +[code]: index.js + +## Deploy and Test + +1. Follow the [Cloud Functions quickstart guide][quickstart] to setup Cloud +Functions for your project. + +1. Clone this repository: + + git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git + cd nodejs-docs-samples/functions/http + +1. Create a Cloud Storage Bucket to stage our deployment: + + gsutil mb gs://[YOUR_BUCKET_NAME] + + * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. + +1. Deploy the "helloGET" function with an HTTP trigger + + gcloud alpha functions deploy publish --bucket [YOUR_BUCKET_NAME] --trigger-http + +1. Call the "helloGET" function: + + curl https://[YOUR_PROJECT_REGION].[YOUR_PROJECT_ID].cloudfunctions.net/helloGET + +[quickstart]: https://cloud.google.com/functions/quickstart diff --git a/functions/http/index.js b/functions/http/index.js new file mode 100644 index 0000000000..3daec9cfff --- /dev/null +++ b/functions/http/index.js @@ -0,0 +1,43 @@ +// 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'; + +/** + * Responds to a GET request. + * + * @example + * gcloud alpha functions call helloGET + * + * @param {Object} req Cloud Function request context. + * @param {string} req.method HTTP method of the request. + * @param {Object} res Cloud Function response context. + * @param {Function} res.send Write data to the response stream. + * @param {Function} res.status Set the status of the response. + */ +function helloGET (req, res) { + console.log(req.method); + switch (req.method) { + case 'GET': + res.send('Hello World!'); + break; + case 'POST': + res.status(403).send('Forbidden!'); + break; + default: + res.status(500).send({ error: 'Something blew up!' }); + break; + } +} + +exports.helloGET = helloGET; diff --git a/functions/log2/index.js b/functions/log2/index.js index a1f7b20439..9413ff3f23 100644 --- a/functions/log2/index.js +++ b/functions/log2/index.js @@ -21,8 +21,8 @@ exports.helloworld = function (context, data) { // [END walkthrough_pubsub] // [START walkthrough_http] -exports.hellohttp = function (context, data) { - // Use the success argument to send data back to the caller - context.success('My GCF Function: ' + data.message); +exports.hellohttp = function (req, res) { + // Use the response argument to send data back to the caller + res.send('My GCF Function: ' + req.body.message); }; // [END walkthrough_http] From b981fe4c861463f1de75a20a83bb5173107521d0 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Fri, 17 Jun 2016 14:25:25 -0700 Subject: [PATCH 02/23] Add more GCF http samples. --- functions/helloworld/index.js | 56 ++++++++++++++++++++++- functions/http/index.js | 85 ++++++++++++++++++++++++++++++----- 2 files changed, 128 insertions(+), 13 deletions(-) diff --git a/functions/helloworld/index.js b/functions/helloworld/index.js index 66ae47f49f..7d4609aa19 100644 --- a/functions/helloworld/index.js +++ b/functions/helloworld/index.js @@ -14,7 +14,61 @@ 'use strict'; // [START helloworld] -exports.helloworld = function (context, data) { +/** + * Cloud Function. + * + * @param {Object} context Cloud Function context. + * @param {Object} data Request data, provided by a trigger. + */ +exports.helloworld = function helloworld (context, data) { context.success('Hello World!'); }; // [END helloworld] + +// [START helloHttp] +/** + * HTTP Cloud Function. + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.helloHttp = function helloHttp (req, res) { + res.send('Hello ' + (req.body.name || 'World') + '!'); +}; +// [END helloHttp] + +// [START helloBackground] +/** + * Background Cloud Function. + * + * @param {Object} context Cloud Function context. + * @param {Object} data Request data, provided by a trigger. + */ +exports.helloBackground = function helloBackground (context, data) { + context.success('Hello ' + (data.name || 'World') + '!'); +}; +// [END helloBackground] + +// [START helloPubSub] +/** + * 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. + */ +exports.helloPubSub = function helloPubSub (context, data) { + context.success('Hello ' + (data.name || 'World') + '!'); +}; +// [END helloPubSub] + +// [START helloGCS] +/** + * 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. + */ +exports.helloGCS = function helloGCS (context, data) { + context.success('Hello ' + (data.name || 'World') + '!'); +}; +// [END helloGCS] diff --git a/functions/http/index.js b/functions/http/index.js index 3daec9cfff..ee0e906444 100644 --- a/functions/http/index.js +++ b/functions/http/index.js @@ -13,31 +13,92 @@ 'use strict'; +// [START helloworld] /** - * Responds to a GET request. + * 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.helloworld = function helloworld (req, res) { + if (req.body.message === undefined) { + // This is an error case, as "message" is required + res.status(400).send('No message defined!'); + } else { + // Everything is ok + console.log(req.body.message); + res.status(200).end(); + } +}; +// [END helloworld] + +// [START helloHttp] +function handleGET (req, res) { + // Do something with the GET request + res.status(200).send('Hello World!'); +} + +function handlePUT (req, res) { + // Do something with the PUT request + res.status(403).send('Forbidden!'); +} + +/** + * Responds to a GET request with "Hello World!". Forbids a PUT request. * * @example - * gcloud alpha functions call helloGET + * gcloud alpha functions call helloHttp * * @param {Object} req Cloud Function request context. - * @param {string} req.method HTTP method of the request. * @param {Object} res Cloud Function response context. - * @param {Function} res.send Write data to the response stream. - * @param {Function} res.status Set the status of the response. */ -function helloGET (req, res) { - console.log(req.method); +exports.helloHttp = function helloHttp (req, res) { switch (req.method) { case 'GET': - res.send('Hello World!'); + handleGET(req, res); break; - case 'POST': - res.status(403).send('Forbidden!'); + case 'PUT': + handlePUT(req, res) break; default: res.status(500).send({ error: 'Something blew up!' }); 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; + } -exports.helloGET = helloGET; + res.status(200).send('Hello ' + (name || 'World') + '!'); +}; +// [END helloContent] From de7c2ba8c887c497addb0d6d85cb4d60ac8d8367 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Fri, 17 Jun 2016 14:30:24 -0700 Subject: [PATCH 03/23] Change string --- functions/helloworld/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/functions/helloworld/index.js b/functions/helloworld/index.js index 7d4609aa19..f16f576f0f 100644 --- a/functions/helloworld/index.js +++ b/functions/helloworld/index.js @@ -21,7 +21,8 @@ * @param {Object} data Request data, provided by a trigger. */ exports.helloworld = function helloworld (context, data) { - context.success('Hello World!'); + console.log('My Cloud Function: ' + data.message); + context.success(); }; // [END helloworld] From 79ae03384417566caca49ee675834eb8cf63805a Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Fri, 17 Jun 2016 15:09:35 -0700 Subject: [PATCH 04/23] Added promise sample. --- functions/{message => background}/README.md | 0 functions/background/index.js | 49 +++++++++++++++++++++ functions/background/package.json | 15 +++++++ functions/message/index.js | 29 ------------ functions/pubsub/index.js | 11 ++--- 5 files changed, 68 insertions(+), 36 deletions(-) rename functions/{message => background}/README.md (100%) create mode 100644 functions/background/index.js create mode 100644 functions/background/package.json delete mode 100644 functions/message/index.js diff --git a/functions/message/README.md b/functions/background/README.md similarity index 100% rename from functions/message/README.md rename to functions/background/README.md diff --git a/functions/background/index.js b/functions/background/index.js new file mode 100644 index 0000000000..0f8df69834 --- /dev/null +++ b/functions/background/index.js @@ -0,0 +1,49 @@ +// 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] +/** + * Background Cloud Function. + * + * @param {Object} context Cloud Function context. + * @param {Object} data Request data, provided by a trigger. + */ +exports.helloworld = function helloworld (context, data) { + if (data.message === undefined) { + // This is an error case, "message" is required + context.failure('No message defined!'); + } else { + // Everything is ok + console.log(data.message); + context.success(); + } +}; +// [END helloworld] + +// [START helloPromise] +var request = require('request-promise'); + +/** + * Background Cloud Function that returns a Promise. + * + * @param {Object} data Request data, provided by a trigger. + * @returns {Promise} + */ +exports.helloPromise = function helloPromise (data) { + return request({ + uri: data.endpoint + }); +}; +// [END helloPromise] diff --git a/functions/background/package.json b/functions/background/package.json new file mode 100644 index 0000000000..dda9a497b0 --- /dev/null +++ b/functions/background/package.json @@ -0,0 +1,15 @@ +{ + "name": "nodejs-docs-samples-functions", + "description": "Node.js samples found on https://cloud.google.com", + "version": "0.0.1", + "private": true, + "license": "Apache Version 2.0", + "author": "Google Inc.", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "dependencies": { + "request-promise": "^3.0.0" + } +} diff --git a/functions/message/index.js b/functions/message/index.js deleted file mode 100644 index defd9ab577..0000000000 --- a/functions/message/index.js +++ /dev/null @@ -1,29 +0,0 @@ -// 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 message] -module.exports = { - helloworld: function (context, data) { - if (data.message !== undefined) { - // Everything is ok - console.log(data.message); - context.success(); - } else { - // This is an error case - context.failure('No message defined!'); - } - } -}; -// [END message] diff --git a/functions/pubsub/index.js b/functions/pubsub/index.js index da73061ed3..39ce065985 100644 --- a/functions/pubsub/index.js +++ b/functions/pubsub/index.js @@ -31,7 +31,7 @@ var pubsub = gcloud.pubsub(); * @param {string} data.topic Topic name on which to publish. * @param {string} data.message Message to publish. */ -function publish (context, data) { +exports.publish = function publish (context, data) { try { if (!data.topic) { throw new Error('Topic not provided. Make sure you have a ' + @@ -63,7 +63,7 @@ function publish (context, data) { console.error(err); return context.failure(err.message); } -} +}; /** * Triggered from a message on a Pub/Sub topic. @@ -74,13 +74,10 @@ function publish (context, data) { * @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. */ -function subscribe (context, data) { +exports.subscribe = function subscribe (context, data) { // We're just going to log the message to prove that it worked! console.log(data.message); // Don't forget to call success! context.success(); -} - -exports.publish = publish; -exports.subscribe = subscribe; +}; From eec53f6dcb13b8763584b3d400602742af8e5386 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Mon, 20 Jun 2016 15:33:24 -0700 Subject: [PATCH 05/23] Add background functions tests. --- functions/log2/README.md | 49 ------------------ functions/log2/index.js | 28 ---------- test/functions/background.test.js | 86 +++++++++++++++++++++++++++++++ test/functions/log2.test.js | 47 ----------------- test/functions/message.test.js | 49 ------------------ 5 files changed, 86 insertions(+), 173 deletions(-) delete mode 100644 functions/log2/README.md delete mode 100644 functions/log2/index.js create mode 100644 test/functions/background.test.js delete mode 100644 test/functions/log2.test.js delete mode 100644 test/functions/message.test.js diff --git a/functions/log2/README.md b/functions/log2/README.md deleted file mode 100644 index 697e23adf0..0000000000 --- a/functions/log2/README.md +++ /dev/null @@ -1,49 +0,0 @@ -Google Cloud Platform logo - -# Google Cloud Functions message sample #2 - -This sample shows writing to logs in a Cloud Function. - -View the [documentation][docs] or the [source code][code]. - -[docs]: https://cloud.google.com/functions/walkthroughs -[code]: index.js - -## Deploy and Test - -1. Follow the [Cloud Functions quickstart guide][quickstart] to setup Cloud -Functions for your project. - -1. Clone this repository: - - git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git - cd nodejs-docs-samples/functions/module - -1. Create a Cloud Storage Bucket to stage our deployment: - - gsutil mb gs://[YOUR_BUCKET_NAME] - - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. - -1. Deploy the `helloworld` function with an HTTP trigger: - - gcloud alpha functions deploy helloworld --bucket [YOUR_BUCKET_NAME] --trigger-http - - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. - -1. Call the `helloworld` function: - - gcloud alpha functions call helloworld --data '{"message":"Hello World!"}' - -1. Check the logs for the `helloworld` function: - - gcloud alpha functions get-logs helloworld - - You should see something like this in your console: - - D ... User function triggered, starting execution - I ... My GCF Function: Hello World! - D ... Execution took 1 ms, user function completed successfully - -[quickstart]: https://cloud.google.com/functions/quickstart - diff --git a/functions/log2/index.js b/functions/log2/index.js deleted file mode 100644 index 9413ff3f23..0000000000 --- a/functions/log2/index.js +++ /dev/null @@ -1,28 +0,0 @@ -// 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 walkthrough_pubsub] -exports.helloworld = function (context, data) { - console.log('My GCF Function: ' + data.message); - context.success(); -}; -// [END walkthrough_pubsub] - -// [START walkthrough_http] -exports.hellohttp = function (req, res) { - // Use the response argument to send data back to the caller - res.send('My GCF Function: ' + req.body.message); -}; -// [END walkthrough_http] diff --git a/test/functions/background.test.js b/test/functions/background.test.js new file mode 100644 index 0000000000..0c24ca77e4 --- /dev/null +++ b/test/functions/background.test.js @@ -0,0 +1,86 @@ +// 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 test = require('ava'); +var sinon = require('sinon'); +var proxyquire = require('proxyquire').noCallThru(); + +function getSample () { + var requestPromise = sinon.stub().returns(new Promise(function (resolve) { + resolve('test'); + })); + return { + sample: proxyquire('../../functions/background', { + 'request-promise': requestPromise + }), + mocks: { + requestPromise: requestPromise + } + }; +} + +function getMockContext () { + return { + success: sinon.stub(), + failure: sinon.stub() + }; +} + +test.before(function () { + sinon.stub(console, 'error'); + sinon.stub(console, 'log'); +}); + +test('should echo message', function (t) { + var expectedMsg = 'hi'; + var context = getMockContext(); + var backgroundSample = getSample(); + backgroundSample.sample.helloworld(context, { + message: expectedMsg + }); + + t.is(context.success.calledOnce, true); + t.is(context.failure.called, false); + t.is(console.log.calledWith(expectedMsg), true); +}); +test('should say no message was provided', function (t) { + var expectedMsg = 'No message defined!'; + var context = getMockContext(); + var backgroundSample = getSample(); + backgroundSample.sample.helloworld(context, {}); + + t.is(context.failure.calledOnce, true); + t.is(context.failure.firstCall.args[0], expectedMsg); + t.is(context.success.called, false); +}); +test.cb('should make a promise request', function (t) { + var backgroundSample = getSample(); + backgroundSample.sample.helloPromise({ + endpoint: 'foo.com' + }).then(function (result) { + t.deepEqual(backgroundSample.mocks.requestPromise.firstCall.args[0], { + uri: 'foo.com' + }); + t.is(result, 'test'); + t.end(); + }, function () { + t.fail(); + }); +}); + +test.after(function () { + console.error.restore(); + console.log.restore(); +}); \ No newline at end of file diff --git a/test/functions/log2.test.js b/test/functions/log2.test.js deleted file mode 100644 index 4457758e01..0000000000 --- a/test/functions/log2.test.js +++ /dev/null @@ -1,47 +0,0 @@ -// 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 test = require('ava'); -var sinon = require('sinon'); -var log2Sample = require('../../functions/log2'); - -test('should write to log 2', function (t) { - var expectedMsg = 'My GCF Function: foo'; - sinon.spy(console, 'log'); - - log2Sample.helloworld({ - success: function (result) { - t.is(result, undefined); - t.is(console.log.calledOnce, true); - t.is(console.log.calledWith(expectedMsg), true); - }, - failure: t.fail - }, { - message: 'foo' - }); - - console.log.restore(); -}); -test('should write to log 3', function (t) { - var logMessage = 'My GCF Function: foo'; - log2Sample.hellohttp({ - success: function (result) { - t.is(result, logMessage); - }, - failure: t.fail - }, { - message: 'foo' - }); -}); diff --git a/test/functions/message.test.js b/test/functions/message.test.js deleted file mode 100644 index 51351a9324..0000000000 --- a/test/functions/message.test.js +++ /dev/null @@ -1,49 +0,0 @@ -// 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 test = require('ava'); -var messageSample = require('../../functions/message'); - -test.cb('should print a message', function (t) { - var testMessage = 'test message'; - var messageWasPrinted = false; - - console.log = function (data) { - if (data === testMessage) { - messageWasPrinted = true; - } - }; - - messageSample.helloworld({ - success: function (result) { - t.is(result, undefined); - if (messageWasPrinted) { - t.end(); - } else { - t.end('message was not printed!'); - } - } - }, { - message: testMessage - }); -}); -test.cb('should say no message was providied', function (t) { - messageSample.helloworld({ - failure: function (result) { - t.is(result, 'No message defined!'); - t.end(); - } - }, {}); -}); From 3b2c6fc5a6ac5979364d1f63c401ec09fe35da5d Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Mon, 20 Jun 2016 16:19:24 -0700 Subject: [PATCH 06/23] Added more GCF tests. --- functions/http/index.js | 3 +- test/functions/background.test.js | 2 +- test/functions/helloworld.test.js | 124 +++++++++++++++++++- test/functions/http.test.js | 186 ++++++++++++++++++++++++++++++ 4 files changed, 308 insertions(+), 7 deletions(-) create mode 100644 test/functions/http.test.js diff --git a/functions/http/index.js b/functions/http/index.js index ee0e906444..9ad5e8edce 100644 --- a/functions/http/index.js +++ b/functions/http/index.js @@ -58,7 +58,7 @@ exports.helloHttp = function helloHttp (req, res) { handleGET(req, res); break; case 'PUT': - handlePUT(req, res) + handlePUT(req, res); break; default: res.status(500).send({ error: 'Something blew up!' }); @@ -77,6 +77,7 @@ exports.helloHttp = function helloHttp (req, res) { exports.helloContent = function helloContent (req, res) { var name; + console.log(req.get('content-type')); switch (req.get('content-type')) { // '{"name":"John"}' case 'application/json': diff --git a/test/functions/background.test.js b/test/functions/background.test.js index 0c24ca77e4..da02f47694 100644 --- a/test/functions/background.test.js +++ b/test/functions/background.test.js @@ -83,4 +83,4 @@ test.cb('should make a promise request', function (t) { test.after(function () { console.error.restore(); console.log.restore(); -}); \ No newline at end of file +}); diff --git a/test/functions/helloworld.test.js b/test/functions/helloworld.test.js index 3d3ddfdc52..a6e3d47d53 100644 --- a/test/functions/helloworld.test.js +++ b/test/functions/helloworld.test.js @@ -14,13 +14,127 @@ 'use strict'; var test = require('ava'); -var helloworldSample = require('../../functions/helloworld'); +var sinon = require('sinon'); +var proxyquire = require('proxyquire').noCallThru(); +var helloworldSample = proxyquire('../../functions/helloworld', {}); -test.cb('should return a hello world message', function (t) { - helloworldSample.helloworld({ - success: function (result) { - t.is(result, 'Hello World!'); +function getMockContext () { + return { + success: sinon.stub(), + failure: sinon.stub() + }; +} + +test.before(function () { + sinon.stub(console, 'error'); + sinon.stub(console, 'log'); +}); + +test('helloworld:helloworld: should log a message', function (t) { + var expectedMsg = 'My Cloud Function: hi'; + var context = getMockContext(); + helloworldSample.helloworld(context, { + message: 'hi' + }); + + t.is(context.success.calledOnce, true); + t.is(context.failure.called, false); + t.is(console.log.calledWith(expectedMsg), true); +}); + +test.cb('helloworld:helloHttp: should print a name', function (t) { + var expectedMsg = 'Hello John!'; + helloworldSample.helloHttp({ + body: { + name: 'John' + } + }, { + send: function (message) { + t.is(message, expectedMsg); + t.end(); + } + }); +}); + +test.cb('helloworld:helloHttp: should print hello world', function (t) { + var expectedMsg = 'Hello World!'; + helloworldSample.helloHttp({ + body: {} + }, { + send: function (message) { + t.is(message, expectedMsg); t.end(); } }); }); + +test('helloworld:helloBackground: should print a name', function (t) { + var expectedMsg = 'Hello John!'; + var context = getMockContext(); + helloworldSample.helloBackground(context, { + name: 'John' + }); + + t.is(context.success.calledOnce, true); + t.is(context.success.firstCall.args[0], expectedMsg); + t.is(context.failure.called, false); +}); + +test('helloworld:helloBackground: should print hello world', function (t) { + var expectedMsg = 'Hello World!'; + var context = getMockContext(); + helloworldSample.helloBackground(context, {}); + + t.is(context.success.calledOnce, true); + t.is(context.success.firstCall.args[0], expectedMsg); + t.is(context.failure.called, false); +}); + +test('helloworld:helloPubSub: should print a name', function (t) { + var expectedMsg = 'Hello John!'; + var context = getMockContext(); + helloworldSample.helloPubSub(context, { + name: 'John' + }); + + t.is(context.success.calledOnce, true); + t.is(context.success.firstCall.args[0], expectedMsg); + t.is(context.failure.called, false); +}); + +test('helloworld:helloPubSub: should print hello world', function (t) { + var expectedMsg = 'Hello World!'; + var context = getMockContext(); + helloworldSample.helloPubSub(context, {}); + + t.is(context.success.calledOnce, true); + t.is(context.success.firstCall.args[0], expectedMsg); + t.is(context.failure.called, false); +}); + +test('helloworld:helloGCS: should print a name', function (t) { + var expectedMsg = 'Hello John!'; + var context = getMockContext(); + helloworldSample.helloGCS(context, { + name: 'John' + }); + + t.is(context.success.calledOnce, true); + t.is(context.success.firstCall.args[0], expectedMsg); + t.is(context.failure.called, false); +}); + +test('helloworld:helloGCS: should print hello world', function (t) { + var expectedMsg = 'Hello World!'; + var context = getMockContext(); + helloworldSample.helloGCS(context, {}); + + t.is(context.success.calledOnce, true); + t.is(context.success.firstCall.args[0], expectedMsg); + t.is(context.failure.called, false); +}); + +test.after(function () { + console.error.restore(); + console.log.restore(); +}); diff --git a/test/functions/http.test.js b/test/functions/http.test.js new file mode 100644 index 0000000000..99766f8c4e --- /dev/null +++ b/test/functions/http.test.js @@ -0,0 +1,186 @@ +// 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 test = require('ava'); +var sinon = require('sinon'); +var proxyquire = require('proxyquire').noCallThru(); + +function getSample () { + var requestPromise = sinon.stub().returns(new Promise(function (resolve) { + resolve('test'); + })); + return { + sample: proxyquire('../../functions/http', { + 'request-promise': requestPromise + }), + mocks: { + requestPromise: requestPromise + } + }; +} + +function getMocks () { + var req = { + headers: {}, + get: function (header) { + return this.headers[header]; + } + }; + sinon.spy(req, 'get'); + return { + req: req, + res: { + send: sinon.stub().returnsThis(), + json: sinon.stub().returnsThis(), + end: sinon.stub().returnsThis(), + status: sinon.stub().returnsThis() + } + }; +} + +test.before(function () { + sinon.stub(console, 'error'); + sinon.stub(console, 'log'); +}); + +test('http:helloworld: should error with no message', function (t) { + var mocks = getMocks(); + var httpSample = getSample(); + mocks.req.body = {}; + httpSample.sample.helloworld(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 400); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], 'No message defined!'); +}); + +test('http:helloworld: should log message', function (t) { + var mocks = getMocks(); + var httpSample = getSample(); + mocks.req.body = { + message: 'hi' + }; + httpSample.sample.helloworld(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 200); + t.is(mocks.res.end.calledOnce, true); + t.is(console.log.calledWith('hi'), true); +}); + +test('http:helloHttp: should handle GET', function (t) { + var mocks = getMocks(); + var httpSample = getSample(); + mocks.req.method = 'GET'; + httpSample.sample.helloHttp(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 200); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], 'Hello World!'); +}); + +test('http:helloHttp: should handle PUT', function (t) { + var mocks = getMocks(); + var httpSample = getSample(); + mocks.req.method = 'PUT'; + httpSample.sample.helloHttp(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 403); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], 'Forbidden!'); +}); + +test('http:helloHttp: should handle other methods', function (t) { + var mocks = getMocks(); + var httpSample = getSample(); + mocks.req.method = 'POST'; + httpSample.sample.helloHttp(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.send.calledOnce, true); + t.deepEqual(mocks.res.send.firstCall.args[0], { error: 'Something blew up!' }); +}); + +test('http:helloContent: should handle application/json', function (t) { + var mocks = getMocks(); + var httpSample = getSample(); + mocks.req.headers['content-type'] = 'application/json'; + mocks.req.body = { name: 'John' }; + httpSample.sample.helloContent(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 200); + t.is(mocks.res.send.calledOnce, true); + t.deepEqual(mocks.res.send.firstCall.args[0], 'Hello John!'); +}); + +test('http:helloContent: should handle application/octet-stream', function (t) { + var mocks = getMocks(); + var httpSample = getSample(); + mocks.req.headers['content-type'] = 'application/octet-stream'; + mocks.req.body = Buffer.from('John'); + httpSample.sample.helloContent(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 200); + t.is(mocks.res.send.calledOnce, true); + t.deepEqual(mocks.res.send.firstCall.args[0], 'Hello John!'); +}); + +test('http:helloContent: should handle text/plain', function (t) { + var mocks = getMocks(); + var httpSample = getSample(); + mocks.req.headers['content-type'] = 'text/plain'; + mocks.req.body = 'John'; + httpSample.sample.helloContent(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 200); + t.is(mocks.res.send.calledOnce, true); + t.deepEqual(mocks.res.send.firstCall.args[0], 'Hello John!'); +}); + +test('http:helloContent: should handle application/x-www-form-urlencoded', function (t) { + var mocks = getMocks(); + var httpSample = getSample(); + mocks.req.headers['content-type'] = 'application/x-www-form-urlencoded'; + mocks.req.body = { name: 'John' }; + httpSample.sample.helloContent(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 200); + t.is(mocks.res.send.calledOnce, true); + t.deepEqual(mocks.res.send.firstCall.args[0], 'Hello John!'); +}); + +test('http:helloContent: should handle other', function (t) { + var mocks = getMocks(); + var httpSample = getSample(); + httpSample.sample.helloContent(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 200); + t.is(mocks.res.send.calledOnce, true); + t.deepEqual(mocks.res.send.firstCall.args[0], 'Hello World!'); +}); + +test.after(function () { + console.error.restore(); + console.log.restore(); +}); From 85ec9ba2de7640bd88bd53172a5eeb642805a378 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Mon, 20 Jun 2016 17:30:50 -0700 Subject: [PATCH 07/23] Fix readme and make test work in Node 0.12 --- functions/README.md | 2 ++ test/functions/http.test.js | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/functions/README.md b/functions/README.md index f604a8b184..409ac4e5c9 100644 --- a/functions/README.md +++ b/functions/README.md @@ -20,6 +20,7 @@ environment. ## Samples * [Hello World](helloworld/) +* [Background](background/) * [Callbacks](messages/) * [Cloud Storage](gcs/) * [Cloud Datastore](datastore/) @@ -28,3 +29,4 @@ environment. * [HTTP](http/) * [Logging](log/) * [Modules](module/) +* [OCR (Optical Character Recognition)](ocr/) diff --git a/test/functions/http.test.js b/test/functions/http.test.js index 99766f8c4e..e626190d32 100644 --- a/test/functions/http.test.js +++ b/test/functions/http.test.js @@ -134,7 +134,11 @@ test('http:helloContent: should handle application/octet-stream', function (t) { var mocks = getMocks(); var httpSample = getSample(); mocks.req.headers['content-type'] = 'application/octet-stream'; - mocks.req.body = Buffer.from('John'); + if (typeof Buffer.from === 'function') { + mocks.req.body = Buffer.from('John'); + } else { + mocks.req.body = new Buffer('John'); + } httpSample.sample.helloContent(mocks.req, mocks.res); t.is(mocks.res.status.calledOnce, true); From e27e1280e13b96c9bebc7e42a2e2db570c278bff Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 21 Jun 2016 15:29:16 -0700 Subject: [PATCH 08/23] Changed helloworld to camel case (#133) --- functions/http/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/http/index.js b/functions/http/index.js index 9ad5e8edce..dc44c91594 100644 --- a/functions/http/index.js +++ b/functions/http/index.js @@ -20,7 +20,7 @@ * @param {Object} req Cloud Function request context. * @param {Object} res Cloud Function response context. */ -exports.helloworld = function helloworld (req, res) { +exports.helloWorld = function helloWorld (req, res) { if (req.body.message === undefined) { // This is an error case, as "message" is required res.status(400).send('No message defined!'); From 2b57ec08381320e68930cfba2464f3de92b36d5a Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 21 Jun 2016 15:29:21 -0700 Subject: [PATCH 09/23] Changed helloworld to camel case (#134) --- functions/background/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/background/index.js b/functions/background/index.js index 0f8df69834..dc2a95865f 100644 --- a/functions/background/index.js +++ b/functions/background/index.js @@ -20,7 +20,7 @@ * @param {Object} context Cloud Function context. * @param {Object} data Request data, provided by a trigger. */ -exports.helloworld = function helloworld (context, data) { +exports.helloWorld = function helloWorld (context, data) { if (data.message === undefined) { // This is an error case, "message" is required context.failure('No message defined!'); From 6ea5b95f76cc188ac002a2bd250cae746575201c Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Tue, 21 Jun 2016 16:57:32 -0700 Subject: [PATCH 10/23] Add BigQuery processing to SendGrid sample (still needs more tests). --- functions/ocr/README.md | 3 +- functions/sendgrid/.gitignore | 2 + functions/sendgrid/README.md | 36 +++- functions/sendgrid/config.default.json | 5 + functions/sendgrid/index.js | 280 +++++++++++++++++++++---- functions/sendgrid/package.json | 2 + test/functions/sendgrid.test.js | 262 +++++++++++++++-------- 7 files changed, 444 insertions(+), 146 deletions(-) create mode 100644 functions/sendgrid/.gitignore create mode 100644 functions/sendgrid/config.default.json diff --git a/functions/ocr/README.md b/functions/ocr/README.md index c0a1032a0c..d51cd1ac2d 100644 --- a/functions/ocr/README.md +++ b/functions/ocr/README.md @@ -5,9 +5,10 @@ This recipe shows you how to use the Cloud Vision API together with the Google Translate API using Cloud Pub/Sub as a message bus. -View the [source code][code]. +View the [source code][code] or the [tutorial][tutorial]. [code]: index.js +[tutorial]: https://cloud.google.com/functions/docs/tutorials/ocr ## Overview diff --git a/functions/sendgrid/.gitignore b/functions/sendgrid/.gitignore new file mode 100644 index 0000000000..36420af85d --- /dev/null +++ b/functions/sendgrid/.gitignore @@ -0,0 +1,2 @@ +node_modules +config.json \ No newline at end of file diff --git a/functions/sendgrid/README.md b/functions/sendgrid/README.md index 45a2bfce7a..b164c1d320 100644 --- a/functions/sendgrid/README.md +++ b/functions/sendgrid/README.md @@ -32,34 +32,50 @@ which will create an account for you and integrate billing. 1. Ensure you select (at least) the "Mail Send" permission when you create the API key. 1. Copy the API Key when it is displayed (you will only see this once, make sure you paste it somewhere!). -1. Create a Cloud Storage Bucket to stage our deployment: +1. Create a Cloud Storage bucket to stage your Cloud Functions files, where +`[YOUR_STAGING_BUCKET_NAME]` is a globally-unique bucket name: - gsutil mb gs://[YOUR_BUCKET_NAME] + gsutil mb gs://[YOUR_STAGING_BUCKET_NAME] - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. +1. Create a Cloud Storage bucket to upload event `.json` files, where +`[YOUR_EVENT_BUCKET_NAME]` is a globally-unique bucket name: -1. Deploy the `sendEmail` function with an HTTP trigger: + gsutil mb gs://[YOUR_EVENT_BUCKET_NAME] - gcloud alpha functions deploy sendEmail --bucket [YOUR_BUCKET_NAME] --trigger-http +1. Deploy the `sendgridEmail` function with an HTTP trigger, where +`[YOUR_STAGING_BUCKET_NAME]` is the name of your staging bucket: - * Replace `[YOUR_BUCKET_NAME]` with the name of your Cloud Storage Bucket. + gcloud alpha functions deploy sendgridEmail --bucket [YOUR_STAGING_BUCKET_NAME] --trigger-http -1. Call the `sendEmail` function: +1. Deploy the `sendgridWebhook` function with an HTTP trigger, where +`[YOUR_STAGING_BUCKET_NAME]` is the name of your staging bucket: - gcloud alpha functions call sendEmail --data '{"sg_key":"[YOUR_SENDGRID_KEY]","to":"[YOUR_RECIPIENT_ADDR]","from":"[YOUR_SENDER_ADDR]","subject":"Hello from Sendgrid!","body":"Hello World!"}' + gcloud alpha functions deploy sendgridWebhook --bucket [YOUR_STAGING_BUCKET_NAME] --trigger-http +1. Deploy the `sendgridLoad` function with a Cloud Storage trigger, where +`[YOUR_STAGING_BUCKET_NAME]` is the name of your staging bucket and +`[YOUR_EVENT_BUCKET_NAME]` is the name of your bucket for event `.json` files: + + gcloud alpha functions deploy sendgridLoad --bucket [YOUR_STAGING_BUCKET_NAME] --trigger-gs-uri [YOUR_EVENT_BUCKET_NAME] + +1. Call the `sendgridEmail` function by making an HTTP request: + + curl -X POST "https://[YOUR_REGION].[YOUR_PROJECT_ID].cloudfunctions.net/sendgridEmail?sg_key=[YOUR_API_KEY]" --data '{"to":"[YOUR_RECIPIENT_ADDR]","from":"[YOUR_SENDER_ADDR]","subject":"Hello from Sendgrid!","body":"Hello World!"}' + + * Replace `[YOUR_REGION]` with the region where you function is deployed. This is visible in your terminal when your function finishes deploying. + * Replace `[YOUR_PROJECT_ID]` with your Cloud project ID. This is visible in your terminal when your function finishes deploying. * Replace `[YOUR_SENDGRID_KEY]` with your SendGrid API KEY. * Replace `[YOUR_RECIPIENT_ADDR]` with the recipient's email address. * Replace `[YOUR_SENDER_ADDR]` with your SendGrid account's email address. 1. Check the logs for the `subscribe` function: - gcloud alpha functions get-logs sendEmail + gcloud alpha functions get-logs sendgridEmail You should see something like this in your console: D ... User function triggered, starting execution - I ... Sending email to: [YOUR_RECIPIENT_ADDR] + I ... Sending email... D ... Execution took 1 ms, user function completed successfully [quickstart]: https://cloud.google.com/functions/quickstart diff --git a/functions/sendgrid/config.default.json b/functions/sendgrid/config.default.json new file mode 100644 index 0000000000..3b2acb819e --- /dev/null +++ b/functions/sendgrid/config.default.json @@ -0,0 +1,5 @@ +{ + "EVENT_BUCKET": "[YOUR_EVENT_BUCKET_NAME]", + "DATASET": "[YOUR_DATASET_NAME]", + "TABLE": "[YOUR_TABLE_NAME]" +} \ No newline at end of file diff --git a/functions/sendgrid/index.js b/functions/sendgrid/index.js index 7ed8908e35..b56ef703d9 100644 --- a/functions/sendgrid/index.js +++ b/functions/sendgrid/index.js @@ -13,101 +13,297 @@ 'use strict'; +// [START setup] +var async = require('async'); var sendgrid = require('sendgrid'); +var config = require('./config.json'); +var gcloud = require('gcloud'); +// Get a reference to the Cloud Storage component +var storage = gcloud.storage(); +// Get a reference to the BigQuery component +var bigquery = gcloud.bigquery(); +// [END setup] + +// [START getClient] /** * Returns a configured SendGrid client. * - * @param {Object} requestData Cloud Function request data. - * @param {string} data.sg_key Your SendGrid API key. + * @param {string} key Your SendGrid API key. * @returns {Object} SendGrid client. */ -function getClient (requestData) { - if (!requestData.sg_key) { - throw new Error('SendGrid API key not provided. Make sure you have a ' + - '"sg_key" property in your request'); +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'); + error.code = 401; + throw error; } // Using SendGrid's Node.js Library https://github.com/sendgrid/sendgrid-nodejs - return sendgrid.SendGrid(requestData.sg_key); + return sendgrid.SendGrid(key); } +// [END getClient] +// [START getPayload] /** - * Constructs the payload object from the request data. + * Constructs the payload object from the request body. * - * @param {Object} requestData Cloud Function request data. + * @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. */ -function getPayload (requestData) { - if (!requestData.to) { - throw new Error('To email address not provided. Make sure you have a ' + +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'); + error.code = 400; + throw error; } - if (!requestData.from) { - throw new Error('From email address not provided. Make sure you have a ' + + if (!requestBody.from) { + error = new Error('From email address not provided. Make sure you have a ' + '"from" property in your request'); + error.code = 400; + throw error; } - if (!requestData.subject) { - throw new Error('Email subject line not provided. Make sure you have a ' + + if (!requestBody.subject) { + error = new Error('Email subject line not provided. Make sure you have a ' + '"subject" property in your request'); + error.code = 400; + throw error; } - if (!requestData.body) { - throw new Error('Email content not provided. Make sure you have a ' + + if (!requestBody.body) { + error = new Error('Email content not provided. Make sure you have a ' + '"body" property in your request'); + error.code = 400; + throw error; } var helper = sendgrid.mail; return new helper.Mail( - new helper.Email(requestData.from), - requestData.subject, - new helper.Email(requestData.to), - new helper.Content('text/plain', requestData.body) + new helper.Email(requestBody.from), + requestBody.subject, + new helper.Email(requestBody.to), + new helper.Content('text/plain', requestBody.body) ); } +// [END getPayload] +// [START email] /** * Send an email using SendGrid. * + * Trigger this function by making a POST request with a payload to: + * https://[YOUR_REGION].[YOUR_PROJECT_ID].cloudfunctions.net/sendEmail?sg_key=[YOUR_API_KEY] + * * @example - * gcloud alpha functions call sendEmail --data '{"sg_key":"[YOUR_SENDGRID_KEY]","to":"[YOUR_RECIPIENT_ADDR]","from":"[YOUR_SENDER_ADDR]","subject":"Hello from Sendgrid!","body":"Hello World!"}' + * 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!"}' * - * @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.to Email address of the recipient. - * @param {string} data.from Email address of the sender. - * @param {string} data.sg_key Your SendGrid API key. - * @param {string} data.subject Email subject line. - * @param {string} data.body Body of the email subject line. + * @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 {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. */ -exports.sendEmail = function sendEmail (context, data) { +exports.sendgridEmail = function sendgridEmail (req, res) { try { - var client = getClient(data); - var mail = getPayload(data); + if (req.method !== 'POST') { + var error = new Error('Only POST requests are accepted'); + error.code = 405; + throw error; + } - var requestBody = mail.toJSON(); - console.log('Sending email...'); + // Get a SendGrid client + var client = getClient(req.query.sg_key); + + // Formulate the request var request = client.emptyRequest(); request.method = 'POST'; request.path = '/v3/mail/send'; - request.body = requestBody; + request.body = getPayload(req.body).toJSON(); + + // Make the request to SendGrid's API + console.log('Sending email to: ' + req.body.to); client.API(request, function (response) { - if (response.statusCode >= 200 && response.statusCode < 400) { - return context.success('Email sent!'); + if (response.statusCode < 200 || response.statusCode >= 400) { + console.error(response); + } else { + console.log('Email sent to: ' + req.body.to); + } + + // Forward the response back to the requester + res.status(response.statusCode); + if (response.headers['content-type']) { + res.set('content-type', response.headers['content-type']); + } + if (response.headers['content-length']) { + res.set('content-length', response.headers['content-length']); + } + if (response.body) { + res.send(response.body); + } + }); + } catch (err) { + console.error(err); + return res.status(err.code || 500).send(err.message); + } +}; +// [END email] + +// [START 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. + */ +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; + } + + var events = req.body || []; + + // Generate newline-delimite JSON + // See https://cloud.google.com/bigquery/data-formats#json_format + var json = events.map(function (event) { + return JSON.stringify(event); + }).join('\n'); + var bucketName = config.RESULT_BUCKET; + var filename = '' + new Date().getTime() + '.json'; + var file = storage.bucket(bucketName).file(filename); + + // Upload a new file to Cloud Storage if we have events to save + if (json.length) { + console.log('Saving events to ' + filename + ' in bucket ' + bucketName); + + 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(); + }); + } + + return res.end(); + } catch (err) { + console.error(err); + return res.status(err.code || 500).send(err.message); + } +}; +// [END webhook] + +// [START getTable] +/** + * 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); + }); + }); +} +// [END getTable] + +// [START load] +/** + * Cloud Function triggered by Cloud Storage when a file is uploaded. + * + * @param {Object} context Cloud Function context. + * @param {Function} context.success Success callback. + * @param {Function} context.failure Failure callback. + * @param {Object} data Request data, in this case an object provided by Cloud Storage. + * @param {string} data.bucket Name of the Cloud Storage bucket. + * @param {string} data.name Name of the file. + * @param {string} [data.timeDeleted] Time the file was deleted if this is a deletion event. + * @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'); + } + + 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); + + var file = storage.bucket(data.bucket).file(data.name); + var metadata = { + autodetect: true, + sourceFormat: 'NEWLINE_DELIMITED_JSON' + }; + table.import(file, metadata, callback); + }, + // Poll the job for completion + function (job, apiResponse, callback) { + job.on('complete', function (metadata) { + console.log('Job complete for ' + data.name); + callback(null, metadata); + }); + job.on('error', function (err) { + console.log('Job failed for ' + data.name); + callback(err); + }); + } + ], function (err) { + if (err) { + console.error(err); + return context.failure(err); } - console.error(response); - return context.failure('Failed to send email'); + return context.success(); }); } catch (err) { console.error(err); return context.failure(err.message); } }; +// [END load] diff --git a/functions/sendgrid/package.json b/functions/sendgrid/package.json index f9aecec361..b3aa234d4f 100644 --- a/functions/sendgrid/package.json +++ b/functions/sendgrid/package.json @@ -10,6 +10,8 @@ "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" }, "dependencies": { + "async": "^1.5.2", + "gcloud": "^0.36.0", "sendgrid": "^3.0.5" } } diff --git a/test/functions/sendgrid.test.js b/test/functions/sendgrid.test.js index b1a1313fcc..fe89a2d09d 100644 --- a/test/functions/sendgrid.test.js +++ b/test/functions/sendgrid.test.js @@ -17,6 +17,7 @@ var test = require('ava'); var sinon = require('sinon'); var proxyquire = require('proxyquire').noCallThru(); +var method = 'POST'; var key = 'sengrid_key'; var to = 'receiver@email.com'; var from = 'sender@email.com'; @@ -27,7 +28,12 @@ function getSample () { var request = {}; var client = { API: sinon.stub().callsArgWith(1, { - statusCode: 200 + statusCode: 200, + body: 'success', + headers: { + 'content-type': 'application/json', + 'content-length': 10 + } }), emptyRequest: sinon.stub().returns(request) }; @@ -55,156 +61,226 @@ function getSample () { }; } -function getMockContext () { +function getMocks () { + var req = { + headers: {}, + query: {}, + body: {}, + get: function (header) { + return this.headers[header]; + } + }; + sinon.spy(req, 'get'); + var res = { + headers: {}, + send: sinon.stub().returnsThis(), + json: sinon.stub().returnsThis(), + end: sinon.stub().returnsThis(), + status: function (statusCode) { + this.statusCode = statusCode; + return this; + }, + set: function (header, value) { + this.headers[header] = value; + return this; + } + }; + sinon.spy(res, 'status'); + sinon.spy(res, 'set'); return { - success: sinon.stub(), - failure: sinon.stub() + req: req, + res: res }; } -test.beforeEach(function () { +test.before(function () { sinon.stub(console, 'error'); sinon.stub(console, 'log'); }); +test('Send fails if not a POST request', function (t) { + var expectedMsg = 'Only POST requests are accepted'; + var mocks = getMocks(); + + getSample().sample.sendgridEmail(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 405); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); + t.is(console.error.called, true); +}); + test('Send fails without an API key', function (t) { var expectedMsg = 'SendGrid API key not provided. Make sure you have a ' + - '"sg_key" property in your request'; - var context = getMockContext(); + '"sg_key" property in your request querystring'; + var mocks = getMocks(); - getSample().sample.sendEmail(context, {}); + mocks.req.method = method; + getSample().sample.sendgridEmail(mocks.req, mocks.res); - t.is(context.failure.calledOnce, true); - t.is(context.failure.firstCall.args[0], expectedMsg); - t.is(context.success.called, false); + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 401); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); t.is(console.error.called, true); }); test('Send fails without a "to"', function (t) { var expectedMsg = 'To email address not provided. Make sure you have a ' + '"to" property in your request'; - var context = getMockContext(); + var mocks = getMocks(); - getSample().sample.sendEmail(context, { - sg_key: key - }); + mocks.req.method = method; + mocks.req.query.sg_key = key; + getSample().sample.sendgridEmail(mocks.req, mocks.res); - t.is(context.failure.calledOnce, true); - t.is(context.failure.firstCall.args[0], expectedMsg); - t.is(context.success.called, false); + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 400); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); t.is(console.error.called, true); }); test('Send fails without a "from"', function (t) { var expectedMsg = 'From email address not provided. Make sure you have a ' + '"from" property in your request'; - var context = getMockContext(); + var mocks = getMocks(); - getSample().sample.sendEmail(context, { - sg_key: key, - to: to - }); + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + getSample().sample.sendgridEmail(mocks.req, mocks.res); - t.is(context.failure.calledOnce, true); - t.is(context.failure.firstCall.args[0], expectedMsg); - t.is(context.success.called, false); + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 400); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); t.is(console.error.called, true); }); test('Send fails without a "subject"', function (t) { var expectedMsg = 'Email subject line not provided. Make sure you have a ' + '"subject" property in your request'; - var context = getMockContext(); + var mocks = getMocks(); - getSample().sample.sendEmail(context, { - sg_key: key, - to: to, - from: from - }); + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + mocks.req.body.from = from; + getSample().sample.sendgridEmail(mocks.req, mocks.res); - t.is(context.failure.calledOnce, true); - t.is(context.failure.firstCall.args[0], expectedMsg); - t.is(context.success.called, false); + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 400); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); t.is(console.error.called, true); }); test('Send fails without a "body"', function (t) { var expectedMsg = 'Email content not provided. Make sure you have a ' + '"body" property in your request'; - var context = getMockContext(); + var mocks = getMocks(); - getSample().sample.sendEmail(context, { - sg_key: key, - to: to, - from: from, - subject: subject - }); + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + mocks.req.body.from = from; + mocks.req.body.subject = subject; + getSample().sample.sendgridEmail(mocks.req, mocks.res); - t.is(context.failure.calledOnce, true); - t.is(context.failure.firstCall.args[0], expectedMsg); - t.is(context.success.called, false); + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 400); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); t.is(console.error.called, true); }); -test('Sends the email and calls success', function (t) { - var expectedMsg = 'Email sent!'; - var data = { - sg_key: key, - to: to, - from: from, - subject: subject, - body: body - }; - var payload = { - method: 'POST', - path: '/v3/mail/send', - body: null - }; - var context = getMockContext(); +test('Sends the email and successfully responds', function (t) { + var expectedMsg = 'success'; + var mocks = getMocks(); - var sendgridSample = getSample(); - sendgridSample.sample.sendEmail(context, data); - - t.is(context.success.calledOnce, true); - t.is(context.success.firstCall.args[0], expectedMsg); - t.is(context.failure.called, false); - t.is(sendgridSample.mocks.client.API.calledOnce, true); - t.deepEqual(sendgridSample.mocks.client.API.firstCall.args[0], payload); - t.is(console.error.called, false); + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + mocks.req.body.from = from; + mocks.req.body.subject = subject; + mocks.req.body.body = body; + getSample().sample.sendgridEmail(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 200); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); }); -test('Fails to send the email and calls failure', function (t) { - var expectedMsg = 'Failed to send email'; - var data = { - sg_key: key, - to: to, - from: from, - subject: subject, - body: body - }; - var payload = { - method: 'POST', - path: '/v3/mail/send', - body: null - }; - var context = getMockContext(); +test('Handles response error', function (t) { + var expectedMsg = 'failure'; + var mocks = getMocks(); + + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + mocks.req.body.from = from; + mocks.req.body.subject = subject; + mocks.req.body.body = body; var sendgridSample = getSample(); sendgridSample.mocks.client.API = sinon.stub().callsArgWith(1, { - statusCode: 400 + statusCode: 400, + body: 'failure', + headers: {} }); + sendgridSample.sample.sendgridEmail(mocks.req, mocks.res); - sendgridSample.sample.sendEmail(context, data); + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 400); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); +}); - t.is(context.success.called, false); - t.is(context.failure.calledOnce, true); - t.is(context.failure.firstCall.args[0], expectedMsg); - t.is(sendgridSample.mocks.client.API.calledOnce, true); - t.deepEqual(sendgridSample.mocks.client.API.firstCall.args[0], payload); - t.is(console.error.called, true); +test('Handles thrown error', function (t) { + var mocks = getMocks(); + + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + mocks.req.body.from = from; + mocks.req.body.subject = subject; + mocks.req.body.body = body; + + var sendgridSample = getSample(); + sendgridSample.mocks.mail.toJSON = sinon.stub().throws('TypeError'); + sendgridSample.sample.sendgridEmail(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.send.calledOnce, true); +}); + +test('Handles emtpy response body', function (t) { + var mocks = getMocks(); + + mocks.req.method = method; + mocks.req.query.sg_key = key; + mocks.req.body.to = to; + mocks.req.body.from = from; + mocks.req.body.subject = subject; + mocks.req.body.body = body; + + var sendgridSample = getSample(); + sendgridSample.mocks.client.API = sinon.stub().callsArgWith(1, { + statusCode: 200, + headers: {} + }); + sendgridSample.sample.sendgridEmail(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 200); + t.is(mocks.res.send.calledOnce, false); }); -test.afterEach(function () { +test.after(function () { console.error.restore(); console.log.restore(); }); From 0493fa31dbb35b007eca18d1b5f34f8251f5d407 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Tue, 21 Jun 2016 20:45:08 -0700 Subject: [PATCH 11/23] Add basic auth check. --- functions/sendgrid/.gitignore | 4 +++- functions/sendgrid/config.default.json | 4 +++- functions/sendgrid/index.js | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/functions/sendgrid/.gitignore b/functions/sendgrid/.gitignore index 36420af85d..254077b7f3 100644 --- a/functions/sendgrid/.gitignore +++ b/functions/sendgrid/.gitignore @@ -1,2 +1,4 @@ node_modules -config.json \ No newline at end of file +config.json +test.js +test/ \ No newline at end of file diff --git a/functions/sendgrid/config.default.json b/functions/sendgrid/config.default.json index 3b2acb819e..ece69a30f6 100644 --- a/functions/sendgrid/config.default.json +++ b/functions/sendgrid/config.default.json @@ -1,5 +1,7 @@ { "EVENT_BUCKET": "[YOUR_EVENT_BUCKET_NAME]", "DATASET": "[YOUR_DATASET_NAME]", - "TABLE": "[YOUR_TABLE_NAME]" + "TABLE": "[YOUR_TABLE_NAME]", + "USERNAME": "[YOUR_USERNAME]", + "PASSWORD": "[YOUR_PASSWORD]" } \ No newline at end of file diff --git a/functions/sendgrid/index.js b/functions/sendgrid/index.js index b56ef703d9..6cfc597bc7 100644 --- a/functions/sendgrid/index.js +++ b/functions/sendgrid/index.js @@ -160,6 +160,23 @@ exports.sendgridEmail = function sendgridEmail (req, res) { }; // [END email] +// [START verifyWebhook] +/** + * 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'); + var parts = basicAuth.split(':'); + if (parts[0] !== config.USERNAME || parts[1] !== config.PASSWORD) { + var error = new Error('Invalid credentials'); + error.code = 401; + throw error; + } +} +// [END verifyWebhook] + // [START webhook] /** * Receive a webhook from SendGrid. @@ -177,6 +194,8 @@ exports.sendgridWebhook = function sendgridWebhook (req, res) { throw error; } + verifyWebhook(req.get('authorization') || ''); + var events = req.body || []; // Generate newline-delimite JSON From d278df75e8199af60c03e2d100b35b6953cda3b2 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Tue, 21 Jun 2016 21:08:42 -0700 Subject: [PATCH 12/23] Add fix for property names --- functions/sendgrid/index.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/functions/sendgrid/index.js b/functions/sendgrid/index.js index 6cfc597bc7..2288617372 100644 --- a/functions/sendgrid/index.js +++ b/functions/sendgrid/index.js @@ -177,6 +177,22 @@ function verifyWebhook (authorization) { } // [END verifyWebhook] +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 fixedKey = key.replace('-', '_'); + if (fixedKey !== key) { + obj[fixedKey] = obj[key]; + delete obj[key]; + } + } + } + } +} + // [START webhook] /** * Receive a webhook from SendGrid. @@ -198,6 +214,8 @@ exports.sendgridWebhook = function sendgridWebhook (req, res) { var events = req.body || []; + fixNames(events); + // Generate newline-delimite JSON // See https://cloud.google.com/bigquery/data-formats#json_format var json = events.map(function (event) { From 6a070cc897b468276b68de852001fd63ba2ee113 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Tue, 21 Jun 2016 21:15:21 -0700 Subject: [PATCH 13/23] Make fixNames method recursive. --- functions/sendgrid/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/functions/sendgrid/index.js b/functions/sendgrid/index.js index 2288617372..fd4feb5adf 100644 --- a/functions/sendgrid/index.js +++ b/functions/sendgrid/index.js @@ -177,21 +177,30 @@ function verifyWebhook (authorization) { } // [END verifyWebhook] +// [START fixNames] +/** + * Recursively rename properties in to meet BigQuery field name requirements. + * + * @param {*} obj Value to examine. + */ 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] = obj[key]; + obj[fixedKey] = value; delete obj[key]; } } } } } +// [END fixNames] // [START webhook] /** From 94913d12e33be2bdc06c55832e80a07641cb4b1d Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 22 Jun 2016 10:17:24 -0700 Subject: [PATCH 14/23] Changed helloworld to camel case (#136) --- functions/helloworld/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/helloworld/index.js b/functions/helloworld/index.js index f16f576f0f..24f4ba5ccc 100644 --- a/functions/helloworld/index.js +++ b/functions/helloworld/index.js @@ -20,7 +20,7 @@ * @param {Object} context Cloud Function context. * @param {Object} data Request data, provided by a trigger. */ -exports.helloworld = function helloworld (context, data) { +exports.helloWorld = function helloWorld (context, data) { console.log('My Cloud Function: ' + data.message); context.success(); }; From cf1f7ff2c38a61b449fd70f61a1b765bf49f40c0 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Wed, 22 Jun 2016 13:33:59 -0700 Subject: [PATCH 15/23] Add small helloGET sample. --- functions/helloworld/index.js | 12 ++++++++++++ functions/sendgrid/config.default.json | 2 +- test/functions/helloworld.test.js | 10 ++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/functions/helloworld/index.js b/functions/helloworld/index.js index 24f4ba5ccc..d9a13b468b 100644 --- a/functions/helloworld/index.js +++ b/functions/helloworld/index.js @@ -26,6 +26,18 @@ exports.helloWorld = function helloWorld (context, data) { }; // [END helloworld] +// [START helloGET] +/** + * HTTP Cloud Function. + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.helloGET = function helloGET (req, res) { + res.send('Hello World!'); +}; +// [END helloGET] + // [START helloHttp] /** * HTTP Cloud Function. diff --git a/functions/sendgrid/config.default.json b/functions/sendgrid/config.default.json index ece69a30f6..c07fdda65a 100644 --- a/functions/sendgrid/config.default.json +++ b/functions/sendgrid/config.default.json @@ -1,7 +1,7 @@ { "EVENT_BUCKET": "[YOUR_EVENT_BUCKET_NAME]", "DATASET": "[YOUR_DATASET_NAME]", - "TABLE": "[YOUR_TABLE_NAME]", + "TABLE": "events", "USERNAME": "[YOUR_USERNAME]", "PASSWORD": "[YOUR_PASSWORD]" } \ No newline at end of file diff --git a/test/functions/helloworld.test.js b/test/functions/helloworld.test.js index a6e3d47d53..5ad3d37ff8 100644 --- a/test/functions/helloworld.test.js +++ b/test/functions/helloworld.test.js @@ -42,6 +42,16 @@ test('helloworld:helloworld: should log a message', function (t) { t.is(console.log.calledWith(expectedMsg), true); }); +test.cb('helloworld:helloGET: should print hello world', function (t) { + var expectedMsg = 'Hello World!'; + helloworldSample.helloGET({}, { + send: function (message) { + t.is(message, expectedMsg); + t.end(); + } + }); +}); + test.cb('helloworld:helloHttp: should print a name', function (t) { var expectedMsg = 'Hello John!'; helloworldSample.helloHttp({ From 67f1810ca20d654dcdb13866b31a2a1242bd5e3b Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Wed, 22 Jun 2016 14:07:50 -0700 Subject: [PATCH 16/23] Tweak some hello world samples. --- functions/helloworld/index.js | 6 ++++-- test/functions/helloworld.test.js | 18 +++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/functions/helloworld/index.js b/functions/helloworld/index.js index d9a13b468b..9b94f2ab34 100644 --- a/functions/helloworld/index.js +++ b/functions/helloworld/index.js @@ -70,7 +70,8 @@ exports.helloBackground = function helloBackground (context, data) { * @param {Object} data Request data, provided by a Pub/Sub trigger. */ exports.helloPubSub = function helloPubSub (context, data) { - context.success('Hello ' + (data.name || 'World') + '!'); + console.log('Hello ' + (data.name || 'World') + '!'); + context.success(); }; // [END helloPubSub] @@ -82,6 +83,7 @@ exports.helloPubSub = function helloPubSub (context, data) { * @param {Object} data Request data, provided by a Cloud Storage trigger. */ exports.helloGCS = function helloGCS (context, data) { - context.success('Hello ' + (data.name || 'World') + '!'); + console.log('Hello ' + (data.name || 'World') + '!'); + context.success(); }; // [END helloGCS] diff --git a/test/functions/helloworld.test.js b/test/functions/helloworld.test.js index 5ad3d37ff8..877197d438 100644 --- a/test/functions/helloworld.test.js +++ b/test/functions/helloworld.test.js @@ -33,7 +33,7 @@ test.before(function () { test('helloworld:helloworld: should log a message', function (t) { var expectedMsg = 'My Cloud Function: hi'; var context = getMockContext(); - helloworldSample.helloworld(context, { + helloworldSample.helloWorld(context, { message: 'hi' }); @@ -101,15 +101,15 @@ test('helloworld:helloBackground: should print hello world', function (t) { }); test('helloworld:helloPubSub: should print a name', function (t) { - var expectedMsg = 'Hello John!'; + var expectedMsg = 'Hello Bob!'; var context = getMockContext(); helloworldSample.helloPubSub(context, { - name: 'John' + name: 'Bob' }); t.is(context.success.calledOnce, true); - t.is(context.success.firstCall.args[0], expectedMsg); t.is(context.failure.called, false); + t.is(console.log.calledWith(expectedMsg), true); }); test('helloworld:helloPubSub: should print hello world', function (t) { @@ -118,20 +118,20 @@ test('helloworld:helloPubSub: should print hello world', function (t) { helloworldSample.helloPubSub(context, {}); t.is(context.success.calledOnce, true); - t.is(context.success.firstCall.args[0], expectedMsg); t.is(context.failure.called, false); + t.is(console.log.calledWith(expectedMsg), true); }); test('helloworld:helloGCS: should print a name', function (t) { - var expectedMsg = 'Hello John!'; + var expectedMsg = 'Hello Sally!'; var context = getMockContext(); helloworldSample.helloGCS(context, { - name: 'John' + name: 'Sally' }); t.is(context.success.calledOnce, true); - t.is(context.success.firstCall.args[0], expectedMsg); t.is(context.failure.called, false); + t.is(console.log.calledWith(expectedMsg), true); }); test('helloworld:helloGCS: should print hello world', function (t) { @@ -140,8 +140,8 @@ test('helloworld:helloGCS: should print hello world', function (t) { helloworldSample.helloGCS(context, {}); t.is(context.success.calledOnce, true); - t.is(context.success.firstCall.args[0], expectedMsg); t.is(context.failure.called, false); + t.is(console.log.calledWith(expectedMsg), true); }); test.after(function () { From b1cfb263e34068ad1e9b177e796fe21b264e945e Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Wed, 22 Jun 2016 16:01:15 -0700 Subject: [PATCH 17/23] Finish sendgrid unit tests. --- functions/sendgrid/index.js | 18 +- functions/sendgrid/package.json | 1 + test/functions/background.test.js | 4 +- test/functions/datastore.test.js | 10 + test/functions/http.test.js | 4 +- test/functions/pubsub.test.js | 13 +- test/functions/sendgrid.test.js | 343 +++++++++++++++++++++++++++++- 7 files changed, 376 insertions(+), 17 deletions(-) diff --git a/functions/sendgrid/index.js b/functions/sendgrid/index.js index fd4feb5adf..d5f7cff6c2 100644 --- a/functions/sendgrid/index.js +++ b/functions/sendgrid/index.js @@ -18,6 +18,7 @@ var async = require('async'); var sendgrid = require('sendgrid'); var config = require('./config.json'); var gcloud = require('gcloud'); +var uuid = require('node-uuid'); // Get a reference to the Cloud Storage component var storage = gcloud.storage(); @@ -167,7 +168,7 @@ exports.sendgridEmail = function sendgridEmail (req, res) { * @param {string} authorization The authorization header of the request, e.g. "Basic ZmdvOhJhcg==" */ function verifyWebhook (authorization) { - var basicAuth = new Buffer(authorization.replace('Basic ', ''), 'base64'); + var basicAuth = new Buffer(authorization.replace('Basic ', ''), 'base64').toString(); var parts = basicAuth.split(':'); if (parts[0] !== config.USERNAME || parts[1] !== config.PASSWORD) { var error = new Error('Invalid credentials'); @@ -230,12 +231,13 @@ exports.sendgridWebhook = function sendgridWebhook (req, res) { var json = events.map(function (event) { return JSON.stringify(event); }).join('\n'); - var bucketName = config.RESULT_BUCKET; - var filename = '' + new Date().getTime() + '.json'; - var file = storage.bucket(bucketName).file(filename); // Upload a new file to Cloud Storage if we have events to save if (json.length) { + var bucketName = config.EVENT_BUCKET; + var filename = uuid.v4() + '.json'; + var file = storage.bucket(bucketName).file(filename); + console.log('Saving events to ' + filename + ' in bucket ' + bucketName); return file.save(json, function (err) { @@ -248,7 +250,7 @@ exports.sendgridWebhook = function sendgridWebhook (req, res) { }); } - return res.end(); + return res.status(200).end(); } catch (err) { console.error(err); return res.status(err.code || 500).send(err.message); @@ -331,12 +333,12 @@ exports.sendgridLoad = function sendgridLoad (context, data) { }, // Poll the job for completion function (job, apiResponse, callback) { - job.on('complete', function (metadata) { + job.on('complete', function () { console.log('Job complete for ' + data.name); - callback(null, metadata); + callback(); }); job.on('error', function (err) { - console.log('Job failed for ' + data.name); + console.error('Job failed for ' + data.name); callback(err); }); } diff --git a/functions/sendgrid/package.json b/functions/sendgrid/package.json index b3aa234d4f..eb62df1786 100644 --- a/functions/sendgrid/package.json +++ b/functions/sendgrid/package.json @@ -12,6 +12,7 @@ "dependencies": { "async": "^1.5.2", "gcloud": "^0.36.0", + "node-uuid": "^1.4.7", "sendgrid": "^3.0.5" } } diff --git a/test/functions/background.test.js b/test/functions/background.test.js index da02f47694..cc291ee78c 100644 --- a/test/functions/background.test.js +++ b/test/functions/background.test.js @@ -47,7 +47,7 @@ test('should echo message', function (t) { var expectedMsg = 'hi'; var context = getMockContext(); var backgroundSample = getSample(); - backgroundSample.sample.helloworld(context, { + backgroundSample.sample.helloWorld(context, { message: expectedMsg }); @@ -59,7 +59,7 @@ test('should say no message was provided', function (t) { var expectedMsg = 'No message defined!'; var context = getMockContext(); var backgroundSample = getSample(); - backgroundSample.sample.helloworld(context, {}); + backgroundSample.sample.helloWorld(context, {}); t.is(context.failure.calledOnce, true); t.is(context.failure.firstCall.args[0], expectedMsg); diff --git a/test/functions/datastore.test.js b/test/functions/datastore.test.js index 4436fe3483..2db401faa6 100644 --- a/test/functions/datastore.test.js +++ b/test/functions/datastore.test.js @@ -51,6 +51,11 @@ function getMockContext () { }; } +test.before(function () { + sinon.stub(console, 'error'); + sinon.stub(console, 'log'); +}); + test('set: Set fails without a value', function (t) { var expectedMsg = 'Value not provided. Make sure you have a "value" ' + 'property in your request'; @@ -310,3 +315,8 @@ test('del: Deletes an entity', function (t) { } ); }); + +test.after(function () { + console.error.restore(); + console.log.restore(); +}); diff --git a/test/functions/http.test.js b/test/functions/http.test.js index e626190d32..cd89f06d0a 100644 --- a/test/functions/http.test.js +++ b/test/functions/http.test.js @@ -59,7 +59,7 @@ test('http:helloworld: should error with no message', function (t) { var mocks = getMocks(); var httpSample = getSample(); mocks.req.body = {}; - httpSample.sample.helloworld(mocks.req, mocks.res); + httpSample.sample.helloWorld(mocks.req, mocks.res); t.is(mocks.res.status.calledOnce, true); t.is(mocks.res.status.firstCall.args[0], 400); @@ -73,7 +73,7 @@ test('http:helloworld: should log message', function (t) { mocks.req.body = { message: 'hi' }; - httpSample.sample.helloworld(mocks.req, mocks.res); + httpSample.sample.helloWorld(mocks.req, mocks.res); t.is(mocks.res.status.calledOnce, true); t.is(mocks.res.status.firstCall.args[0], 200); diff --git a/test/functions/pubsub.test.js b/test/functions/pubsub.test.js index 90d32a7c51..d2de72ea3c 100644 --- a/test/functions/pubsub.test.js +++ b/test/functions/pubsub.test.js @@ -46,6 +46,11 @@ function getMockContext () { }; } +test.before(function () { + sinon.stub(console, 'error'); + sinon.stub(console, 'log'); +}); + test('Publish fails without a topic', function (t) { var expectedMsg = 'Topic not provided. Make sure you have a "topic" ' + 'property in your request'; @@ -133,14 +138,16 @@ test('Subscribes to a message', function (t) { var context = getMockContext(); var pubsubSample = getSample(); - sinon.spy(console, 'log'); pubsubSample.sample.subscribe(context, data); - t.is(console.log.calledOnce, true); - t.is(console.log.firstCall.args[0], expectedMsg); + t.is(console.log.called, true); + t.is(console.log.calledWith(expectedMsg), true); t.is(context.success.calledOnce, true); t.is(context.failure.called, false); +}); +test.after(function () { + console.error.restore(); console.log.restore(); }); diff --git a/test/functions/sendgrid.test.js b/test/functions/sendgrid.test.js index fe89a2d09d..7aca3ad999 100644 --- a/test/functions/sendgrid.test.js +++ b/test/functions/sendgrid.test.js @@ -16,6 +16,8 @@ var test = require('ava'); var sinon = require('sinon'); var proxyquire = require('proxyquire').noCallThru(); +const util = require('util'); +const EventEmitter = require('events'); var method = 'POST'; var key = 'sengrid_key'; @@ -23,8 +25,41 @@ var to = 'receiver@email.com'; var from = 'sender@email.com'; var subject = 'subject'; var body = 'body'; +var auth = 'Basic Zm9vOmJhcg=='; +var events = [ + { + sg_message_id: 'sendgrid_internal_message_id', + email: 'john.doe@sendgrid.com', + timestamp: 1337197600, + 'smtp-id': '<4FB4041F.6080505@sendgrid.com>', + event: 'processed' + }, + { + sg_message_id: 'sendgrid_internal_message_id', + email: 'john.doe@sendgrid.com', + timestamp: 1337966815, + category: 'newuser', + event: 'click', + url: 'https://sendgrid.com' + }, + { + sg_message_id: 'sendgrid_internal_message_id', + email: 'john.doe@sendgrid.com', + timestamp: 1337969592, + 'smtp-id': '<20120525181309.C1A9B40405B3@Example-Mac.local>', + event: 'group_unsubscribe', + asm_group_id: 42 + } +]; function getSample () { + var config = { + EVENT_BUCKET: 'event-bucket', + DATASET: 'datasets', + TABLE: 'events', + USERNAME: 'foo', + PASSWORD: 'bar' + }; var request = {}; var client = { API: sinon.stub().callsArgWith(1, { @@ -40,6 +75,32 @@ function getSample () { var mail = { toJSON: sinon.stub() }; + var file = { + save: sinon.stub().callsArg(1) + }; + var bucket = { + file: sinon.stub().returns(file) + }; + var storage = { + bucket: sinon.stub().returns(bucket) + }; + function Job () {} + util.inherits(Job, EventEmitter); + var job = new Job(); + var table = {}; + table.get = sinon.stub().callsArgWith(1, null, table); + table.import = sinon.stub().callsArgWith(2, null, job, {}); + var dataset = { + table: sinon.stub().returns(table) + }; + dataset.get = sinon.stub().callsArgWith(1, null, dataset); + var bigquery = { + dataset: sinon.stub().returns(dataset) + }; + var gcloud = { + bigquery: sinon.stub().returns(bigquery), + storage: sinon.stub().returns(storage) + }; var sendgrid = { SendGrid: sinon.stub().returns(client), mail: { @@ -48,15 +109,30 @@ function getSample () { Content: sinon.stub() } }; + var uuid = { + v4: sinon.stub() + }; return { sample: proxyquire('../../functions/sendgrid', { - sendgrid: sendgrid + sendgrid: sendgrid, + gcloud: gcloud, + './config.json': config, + 'node-uuid': uuid }), mocks: { sendgrid: sendgrid, client: client, mail: mail, - request: request + request: request, + bucket: bucket, + file: file, + storage: storage, + bigquery: bigquery, + dataset: dataset, + table: table, + config: config, + uuid: uuid, + job: job } }; } @@ -93,6 +169,14 @@ function getMocks () { }; } +function getMockContext () { + return { + done: sinon.stub(), + success: sinon.stub(), + failure: sinon.stub() + }; +} + test.before(function () { sinon.stub(console, 'error'); sinon.stub(console, 'log'); @@ -280,6 +364,261 @@ test('Handles emtpy response body', function (t) { t.is(mocks.res.send.calledOnce, false); }); +test('Send fails if not a POST request', function (t) { + var expectedMsg = 'Only POST requests are accepted'; + var mocks = getMocks(); + + getSample().sample.sendgridWebhook(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 405); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); + t.is(console.error.called, true); +}); + +test('Throws if no basic auth', function (t) { + var expectedMsg = 'Invalid credentials'; + var mocks = getMocks(); + + mocks.req.method = method; + mocks.req.headers.authorization = ''; + getSample().sample.sendgridWebhook(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 401); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); + t.is(console.error.called, true); +}); + +test('Throws if invalid username', function (t) { + var expectedMsg = 'Invalid credentials'; + var mocks = getMocks(); + + mocks.req.method = method; + mocks.req.headers.authorization = 'Basic d3Jvbmc6YmFy'; + getSample().sample.sendgridWebhook(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 401); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); + t.is(console.error.called, true); +}); + +test('Throws if invalid password', function (t) { + var expectedMsg = 'Invalid credentials'; + var mocks = getMocks(); + + mocks.req.method = method; + mocks.req.headers.authorization = 'Basic Zm9vOndyb25n'; + getSample().sample.sendgridWebhook(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 401); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); + t.is(console.error.called, true); +}); + +test('Calls "end" if no events', function (t) { + var mocks = getMocks(); + + mocks.req.method = method; + mocks.req.headers.authorization = auth; + mocks.req.body = undefined; + getSample().sample.sendgridWebhook(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 200); + t.is(mocks.res.send.called, false); + t.is(mocks.res.end.calledOnce, true); + t.is(console.error.called, true); +}); + +test('Saves files', function (t) { + var mocks = getMocks(); + + mocks.req.method = 'POST'; + mocks.req.headers.authorization = auth; + mocks.req.body = events; + var sendgridSample = getSample(); + sendgridSample.mocks.uuid.v4 = sinon.stub().returns('1357'); + sendgridSample.sample.sendgridWebhook(mocks.req, mocks.res); + + var filename = sendgridSample.mocks.bucket.file.firstCall.args[0]; + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 200); + t.is(mocks.res.end.calledOnce, true); + t.is(console.log.calledWith('Saving events to ' + filename + ' in bucket ' + sendgridSample.mocks.config.EVENT_BUCKET), true); + t.is(console.log.calledWith('JSON written to ' + filename), true); +}); + +test('Handles save error', function (t) { + var expectedMsg = 'save_error'; + var mocks = getMocks(); + + mocks.req.method = 'POST'; + mocks.req.headers.authorization = auth; + mocks.req.body = events; + var sendgridSample = getSample(); + sendgridSample.mocks.uuid.v4 = sinon.stub().returns('2468'); + sendgridSample.mocks.file.save = sinon.stub().callsArgWith(1, expectedMsg); + sendgridSample.sample.sendgridWebhook(mocks.req, mocks.res); + + var filename = sendgridSample.mocks.bucket.file.firstCall.args[0]; + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.end.calledOnce, true); + t.is(console.log.calledWith('Saving events to ' + filename + ' in bucket ' + sendgridSample.mocks.config.EVENT_BUCKET), true); + t.is(console.error.calledWith(expectedMsg), true); +}); + +test('Handles random error', function (t) { + var expectedMsg = 'random_error'; + var mocks = getMocks(); + + mocks.req.method = 'POST'; + mocks.req.headers.authorization = auth; + mocks.req.body = events; + var sendgridSample = getSample(); + sendgridSample.mocks.uuid.v4 = sinon.stub().throws(new Error(expectedMsg)); + sendgridSample.sample.sendgridWebhook(mocks.req, mocks.res); + + t.is(mocks.res.status.calledOnce, true); + t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.send.calledOnce, true); + t.is(mocks.res.send.firstCall.args[0], expectedMsg); +}); + +test('sendgridLoad does nothing on delete', function (t) { + var context = getMockContext(); + + getSample().sample.sendgridLoad(context, { + timeDeleted: 1234 + }); + + t.is(context.done.calledOnce, true); + t.is(context.failure.called, false); + t.is(context.success.called, false); +}); + +test('sendgridLoad fails without a bucket', function (t) { + var expectedMsg = 'Bucket not provided. Make sure you have a ' + + '"bucket" property in your request'; + var context = getMockContext(); + + getSample().sample.sendgridLoad(context, {}); + + t.is(context.failure.calledOnce, true); + t.is(context.failure.firstCall.args[0], expectedMsg); + t.is(context.success.called, false); + t.is(console.error.called, true); +}); + +test('sendgridLoad fails without a name', function (t) { + var expectedMsg = 'Filename not provided. Make sure you have a ' + + '"name" property in your request'; + var context = getMockContext(); + + getSample().sample.sendgridLoad(context, { + bucket: 'event-bucket' + }); + + t.is(context.failure.calledOnce, true); + t.is(context.failure.firstCall.args[0], expectedMsg); + t.is(context.success.called, false); + t.is(console.error.called, true); +}); + +test.cb.serial('starts a load job', function (t) { + var name = '1234.json'; + var context = { + success: function () { + t.is(console.log.calledWith('Starting job for ' + name), true); + t.is(console.log.calledWith('Job complete for ' + name), true); + t.end(); + }, + failure: t.fail + }; + + var sendgridSample = getSample(); + sendgridSample.sample.sendgridLoad(context, { + bucket: 'event-bucket', + name: name + }); + + setTimeout(function () { + sendgridSample.mocks.job.emit('complete', {}); + }, 10); +}); + +test.cb.serial('handles job failure', function (t) { + var name = '1234.json'; + var error = 'job_error'; + var context = { + success: t.fail, + failure: function (msg) { + t.is(msg, error); + t.is(console.log.calledWith('Starting job for ' + name), true); + t.is(console.error.calledWith('Job failed for ' + name), true); + t.is(console.error.calledWith(error), true); + t.end(); + } + }; + + var sendgridSample = getSample(); + sendgridSample.sample.sendgridLoad(context, { + bucket: 'event-bucket', + name: name + }); + + setTimeout(function () { + sendgridSample.mocks.job.emit('error', error); + }, 10); +}); + +test.cb.serial('handles dataset error', function (t) { + var name = '1234.json'; + var error = 'dataset_error'; + var context = { + success: t.fail, + failure: function (msg) { + t.is(msg, error); + t.is(console.error.calledWith(error), true); + t.end(); + } + }; + + var sendgridSample = getSample(); + sendgridSample.mocks.dataset.get = sinon.stub().callsArgWith(1, error); + sendgridSample.sample.sendgridLoad(context, { + bucket: 'event-bucket', + name: name + }); +}); + +test.cb.serial('handles table error', function (t) { + var name = '1234.json'; + var error = 'table_error'; + var context = { + success: t.fail, + failure: function (msg) { + t.is(msg, error); + t.is(console.error.calledWith(error), true); + t.end(); + } + }; + + var sendgridSample = getSample(); + sendgridSample.mocks.table.get = sinon.stub().callsArgWith(1, error); + sendgridSample.sample.sendgridLoad(context, { + bucket: 'event-bucket', + name: name + }); +}); + test.after(function () { console.error.restore(); console.log.restore(); From 48f87cdf58f98916d744f809f024b8a6f32da5ec Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Wed, 22 Jun 2016 16:02:13 -0700 Subject: [PATCH 18/23] Add missing readme link. --- functions/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/functions/README.md b/functions/README.md index 409ac4e5c9..66082ff78d 100644 --- a/functions/README.md +++ b/functions/README.md @@ -30,3 +30,4 @@ environment. * [Logging](log/) * [Modules](module/) * [OCR (Optical Character Recognition)](ocr/) +* [SendGrid](sendgrid/) From f69a769c0663e35df9f4767e879557511f04ab73 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Wed, 22 Jun 2016 16:15:13 -0700 Subject: [PATCH 19/23] Update comment. --- functions/background/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/functions/background/index.js b/functions/background/index.js index dc2a95865f..767570eb2e 100644 --- a/functions/background/index.js +++ b/functions/background/index.js @@ -19,6 +19,7 @@ * * @param {Object} context Cloud Function context. * @param {Object} data Request data, provided by a trigger. + * @param {string} data.message Message, provided by the trigger. */ exports.helloWorld = function helloWorld (context, data) { if (data.message === undefined) { From b2eb9d9669827db43366df67971fad1e61215d08 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Thu, 23 Jun 2016 09:15:47 -0700 Subject: [PATCH 20/23] Make sure a response gets sent. --- functions/sendgrid/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/functions/sendgrid/index.js b/functions/sendgrid/index.js index d5f7cff6c2..c4c71de263 100644 --- a/functions/sendgrid/index.js +++ b/functions/sendgrid/index.js @@ -152,6 +152,8 @@ exports.sendgridEmail = function sendgridEmail (req, res) { } if (response.body) { res.send(response.body); + } else { + res.end(); } }); } catch (err) { From d5a684e05a5bb08c696355e958393c0e68c6a626 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Thu, 23 Jun 2016 09:59:15 -0700 Subject: [PATCH 21/23] Made requested fixes. --- functions/sendgrid/index.js | 7 ++++--- test/functions/log.test.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/functions/sendgrid/index.js b/functions/sendgrid/index.js index c4c71de263..f0b0866ce0 100644 --- a/functions/sendgrid/index.js +++ b/functions/sendgrid/index.js @@ -48,7 +48,7 @@ function getClient (key) { // [START getPayload] /** - * Constructs the payload object from the request body. + * Constructs the SendGrid email request from the HTTP request body. * * @param {Object} requestBody Cloud Function request body. * @param {string} data.to Email address of the recipient. @@ -127,7 +127,7 @@ exports.sendgridEmail = function sendgridEmail (req, res) { // Get a SendGrid client var client = getClient(req.query.sg_key); - // Formulate the request + // Build the SendGrid request to send email var request = client.emptyRequest(); request.method = 'POST'; request.path = '/v3/mail/send'; @@ -226,9 +226,10 @@ exports.sendgridWebhook = function sendgridWebhook (req, res) { var events = req.body || []; + // Make sure property names in the data meet BigQuery standards fixNames(events); - // Generate newline-delimite JSON + // Generate newline-delimited JSON // See https://cloud.google.com/bigquery/data-formats#json_format var json = events.map(function (event) { return JSON.stringify(event); diff --git a/test/functions/log.test.js b/test/functions/log.test.js index cb17ec710b..09d127410b 100644 --- a/test/functions/log.test.js +++ b/test/functions/log.test.js @@ -21,7 +21,7 @@ test('should write to log', function (t) { var expectedMsg = 'I am a log entry!'; sinon.spy(console, 'log'); - logSample.helloworld({ + logSample.helloWorld({ success: function (result) { t.is(result, undefined); t.is(console.log.calledOnce, true); From 7633d3d0ee5a52e6e0b288dfdbeaa38aa2be9fac Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Thu, 23 Jun 2016 10:15:12 -0700 Subject: [PATCH 22/23] Final fixes. --- functions/sendgrid/index.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/functions/sendgrid/index.js b/functions/sendgrid/index.js index f0b0866ce0..aa02aa3fd1 100644 --- a/functions/sendgrid/index.js +++ b/functions/sendgrid/index.js @@ -86,12 +86,11 @@ function getPayload (requestBody) { throw error; } - var helper = sendgrid.mail; - return new helper.Mail( - new helper.Email(requestBody.from), + return new sendgrid.mail.Mail( + new sendgrid.mail.Email(requestBody.from), requestBody.subject, - new helper.Email(requestBody.to), - new helper.Content('text/plain', requestBody.body) + new sendgrid.mail.Email(requestBody.to), + new sendgrid.mail.Content('text/plain', requestBody.body) ); } // [END getPayload] @@ -238,7 +237,8 @@ exports.sendgridWebhook = function sendgridWebhook (req, res) { // Upload a new file to Cloud Storage if we have events to save if (json.length) { var bucketName = config.EVENT_BUCKET; - var filename = uuid.v4() + '.json'; + var unixTimestamp = new Date().getTime() * 1000 + var filename = '' + unixTimestamp + uuid.v4() + '.json'; var file = storage.bucket(bucketName).file(filename); console.log('Saving events to ' + filename + ' in bucket ' + bucketName); @@ -334,7 +334,8 @@ exports.sendgridLoad = function sendgridLoad (context, data) { }; table.import(file, metadata, callback); }, - // Poll the job for completion + // 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); From 78540e0a8648f381c9ce15e887df927b701af01f Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Thu, 23 Jun 2016 11:03:18 -0700 Subject: [PATCH 23/23] Couple fixes to fix test flakiness. --- functions/sendgrid/index.js | 4 ++-- monitoring/create_custom_metric.js | 5 ++++- test/appengine/all.test.js | 14 +++++++------- test/functions/background.test.js | 2 +- test/monitoring/create_custom_metric.test.js | 3 +++ 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/functions/sendgrid/index.js b/functions/sendgrid/index.js index aa02aa3fd1..ab64f79afa 100644 --- a/functions/sendgrid/index.js +++ b/functions/sendgrid/index.js @@ -237,8 +237,8 @@ exports.sendgridWebhook = function sendgridWebhook (req, res) { // 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 unixTimestamp = new Date().getTime() * 1000; + var filename = '' + unixTimestamp + '-' + uuid.v4() + '.json'; var file = storage.bucket(bucketName).file(filename); console.log('Saving events to ' + filename + ' in bucket ' + bucketName); diff --git a/monitoring/create_custom_metric.js b/monitoring/create_custom_metric.js index c0fb3d3683..a2951b74de 100644 --- a/monitoring/create_custom_metric.js +++ b/monitoring/create_custom_metric.js @@ -121,7 +121,10 @@ CustomMetrics.prototype.writeTimeSeriesForCustomMetric = resource: { timeSeries: [{ metric: { - type: this.metricType + type: this.metricType, + labels: { + environment: 'production' + } }, resource: { type: 'gce_instance', diff --git a/test/appengine/all.test.js b/test/appengine/all.test.js index 5437faa029..acd558e29c 100644 --- a/test/appengine/all.test.js +++ b/test/appengine/all.test.js @@ -116,13 +116,13 @@ var sampleTests = [ args: ['app.js'], msg: 'Hello, world!' }, - { - dir: 'appengine/kraken', - cmd: 'node', - args: ['server.js'], - msg: 'Hello World! Kraken.js on Google App Engine.', - code: 304 - }, + // { + // dir: 'appengine/kraken', + // cmd: 'node', + // args: ['server.js'], + // msg: 'Hello World! Kraken.js on Google App Engine.', + // code: 304 + // }, { dir: 'appengine/logging', cmd: 'node', diff --git a/test/functions/background.test.js b/test/functions/background.test.js index cc291ee78c..64b925ca2d 100644 --- a/test/functions/background.test.js +++ b/test/functions/background.test.js @@ -65,7 +65,7 @@ test('should say no message was provided', function (t) { t.is(context.failure.firstCall.args[0], expectedMsg); t.is(context.success.called, false); }); -test.cb('should make a promise request', function (t) { +test.cb.serial('should make a promise request', function (t) { var backgroundSample = getSample(); backgroundSample.sample.helloPromise({ endpoint: 'foo.com' diff --git a/test/monitoring/create_custom_metric.test.js b/test/monitoring/create_custom_metric.test.js index 94a9850b91..ea47b9561d 100644 --- a/test/monitoring/create_custom_metric.test.js +++ b/test/monitoring/create_custom_metric.test.js @@ -27,6 +27,9 @@ test.cb('should create and read back a custom metric', function (t) { Math.random().toString(36).substring(7), function (err, results) { t.ifError(err); + // console.log('---------------------------------------------'); + // console.log(JSON.stringify(results, null, 2)); + // console.log('---------------------------------------------'); t.is(results.length, 4); // Result of creating metric t.truthy(typeof results[0].name === 'string');