diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg new file mode 100644 index 0000000000..881f2f9de4 --- /dev/null +++ b/.kokoro/common.cfg @@ -0,0 +1,13 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Download trampoline resources. These will be in ${KOKORO_GFILE_DIR} +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# All builds use the trampoline script to run in docker. +build_file: "nodejs-docs-samples/.kokoro/trampoline.sh" + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-docs-samples/nodejs" +} \ No newline at end of file diff --git a/.kokoro/functions-helloworld.cfg b/.kokoro/functions-helloworld.cfg new file mode 100644 index 0000000000..7d018ff1e2 --- /dev/null +++ b/.kokoro/functions-helloworld.cfg @@ -0,0 +1,10 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Download secrets from Cloud Storage. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/nodejs-docs-samples" + +# Tell the trampoline which build file to use. +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/nodejs-docs-samples/.kokoro/functions-helloworld.sh" +} diff --git a/.kokoro/functions-helloworld.sh b/.kokoro/functions-helloworld.sh new file mode 100755 index 0000000000..aac336cc7d --- /dev/null +++ b/.kokoro/functions-helloworld.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Copyright 2017 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. + +export GCLOUD_PROJECT=nodejs-docs-samples-tests +STAGE_BUCKET=$GCLOUD_PROJECT +GCP_REGION=us-central1 +FUNCTIONS_TOPIC=integration-test-functions +FUNCTIONS_BUCKET=$FUNCTIONS_TOPIC +export BASE_URL=https://${GCP_REGION}-${GCLOUD_PROJECT}.cloudfunctions.net + +cd github/nodejs-docs-samples/functions/helloworld + +# Install dependencies +npm install + +# Configure gcloud +export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/secrets-key.json +gcloud auth activate-service-account --key-file "$GOOGLE_APPLICATION_CREDENTIALS" +gcloud config set project $GCLOUD_PROJECT + +function cleanup { + CODE=$? + + gcloud beta functions delete helloHttp -q + gcloud beta functions delete helloGET -q + gcloud beta functions delete helloBackground -q + gcloud beta functions delete helloPubSub -q + gcloud beta functions delete helloGCS -q + gcloud beta functions delete helloError -q + gcloud beta functions delete helloError2 -q + gcloud beta functions delete helloError3 -q + gcloud beta functions delete helloTemplate -q +} +trap cleanup EXIT + +set -e + +# Deploy + run the functions +npm run e2e-test \ No newline at end of file diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh new file mode 100755 index 0000000000..0ac4f9af0f --- /dev/null +++ b/.kokoro/trampoline.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Copyright 2017 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. +python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" diff --git a/circle.yml b/circle.yml index 6f809b3a78..780f05a5d5 100644 --- a/circle.yml +++ b/circle.yml @@ -18,7 +18,7 @@ machine: node: - version: 6.11.2 + version: 6.12.3 # Use for broader build-related configuration general: @@ -31,21 +31,21 @@ dependencies: override: - echo $KEYFILE > /home/ubuntu/nodejs-docs-samples/key.json - gcloud auth activate-service-account --key-file /home/ubuntu/nodejs-docs-samples/key.json || true - - yarn global add ava nyc codecov semistandard @google-cloud/nodejs-repo-tools@1.4.17 + - yarn global add ava nyc codecov semistandard @google-cloud/nodejs-repo-tools@1.4.17 @google-cloud/functions-emulator@1.0.0-alpha.29 - yarn install - yarn run lint - samples test install -l=functions/background - - samples test install -l=functions/datastore - samples test install -l=functions/errorreporting - samples test install -l=functions/gcs + - samples test install -l=functions/datastore - samples test install -l=functions/helloworld - - samples test install -l=functions/http - samples test install -l=functions/imagemagick - samples test install -l=functions/log - samples test install -l=functions/ocr/app - samples test install -l=functions/pubsub - samples test install -l=functions/sendgrid - samples test install -l=functions/slack + - samples test install -l=functions/spanner - samples test install -l=functions/uuid cache_directories: - ~/.cache/yarn @@ -61,12 +61,24 @@ dependencies: - functions/pubsub/node_modules - functions/sendgrid/node_modules - functions/slack/node_modules + - functions/spanner/node_modules - functions/uuid/node_modules # Run your tests test: override: - - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/**/test/**/*.test.js' + - functions start && cd functions/datastore && npm run system-test + - functions start && cd functions/helloworld && npm run system-test + - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/background/test/**/*.test.js' + - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/gcs/test/**/*.test.js' + - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/http/test/**/*.test.js' + - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/imagemagick/test/**/*.test.js' + - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/log/test/**/*.test.js' + - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/pubsub/test/**/*.test.js' + - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/sendgrid/test/**/*.test.js' + - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/slack/test/**/*.test.js' + - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/spanner/test/**/*.test.js' + - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/uuid/test/**/*.test.js' post: - nyc report --reporter=lcov > coverage.lcov && codecov || true deployment: diff --git a/functions/background/package.json b/functions/background/package.json index bba4020599..212f354255 100644 --- a/functions/background/package.json +++ b/functions/background/package.json @@ -17,14 +17,14 @@ "test": "ava -T 20s --verbose test/*.test.js" }, "dependencies": { - "request": "2.81.0", - "request-promise": "4.2.1" + "request": "2.83.0", + "request-promise": "4.2.2" }, "devDependencies": { - "@google-cloud/nodejs-repo-tools": "1.4.17", - "ava": "0.21.0", + "@google-cloud/nodejs-repo-tools": "2.1.0", + "ava": "0.23.0", "proxyquire": "1.8.0", - "sinon": "3.2.0" + "sinon": "4.0.2" }, "cloud-repo-tools": { "requiresKeyFile": true, diff --git a/functions/datastore/index.js b/functions/datastore/index.js index 7654ee164d..0fa60c988b 100644 --- a/functions/datastore/index.js +++ b/functions/datastore/index.js @@ -69,7 +69,7 @@ exports.set = (req, res) => { .then(() => res.status(200).send(`Entity ${key.path.join('/')} saved.`)) .catch((err) => { console.error(err); - res.status(500).send(err); + res.status(500).send(err.message); return Promise.reject(err); }); }; @@ -92,7 +92,7 @@ exports.get = (req, res) => { return datastore.get(key) .then(([entity]) => { // The get operation will not fail for a non-existent entity, it just - // returns null. + // returns an empty dictionary. if (!entity) { throw new Error(`No entity found for key ${key.path.join('/')}.`); } @@ -101,7 +101,7 @@ exports.get = (req, res) => { }) .catch((err) => { console.error(err); - res.status(500).send(err); + res.status(500).send(err.message); return Promise.reject(err); }); }; @@ -122,11 +122,13 @@ exports.del = (req, res) => { const key = getKeyFromRequestData(req.body); // Deletes the entity + // The delete operation will not fail for a non-existent entity, it just + // doesn't delete anything return datastore.delete(key) .then(() => res.status(200).send(`Entity ${key.path.join('/')} deleted.`)) .catch((err) => { console.error(err); res.status(500).send(err); - return Promise.reject(err); + return Promise.reject(err.message); }); }; diff --git a/functions/datastore/package.json b/functions/datastore/package.json index 4914ac3ee0..1d455aea8f 100644 --- a/functions/datastore/package.json +++ b/functions/datastore/package.json @@ -12,21 +12,30 @@ "node": ">=4.3.2" }, "scripts": { - "lint": "samples lint", + "lint": "repo-tools lint", "pretest": "npm run lint", - "test": "ava -T 20s --verbose test/*.test.js" + "e2e-test": "export FUNCTIONS_CMD='gcloud beta functions' && sh test/updateFunctions.sh && BASE_URL=\"https://$GCF_REGION-$GCLOUD_PROJECT.cloudfunctions.net/\" ava -T 20s --verbose test/*.test.js", + "system-test": "export FUNCTIONS_CMD='functions' && sh test/updateFunctions.sh && BASE_URL=\"http://localhost:8010/$GCLOUD_PROJECT/$GCF_REGION\" ava -T 20s --verbose test/*.test.js", + "test": "npm run system-test" }, "dependencies": { - "@google-cloud/datastore": "1.1.0" + "@google-cloud/datastore": "1.3.3", + "supertest": "^3.0.0" }, "devDependencies": { - "@google-cloud/nodejs-repo-tools": "1.4.17", - "ava": "0.21.0", + "@google-cloud/functions-emulator": "^1.0.0-alpha.29", + "@google-cloud/nodejs-repo-tools": "2.1.3", + "ava": "0.24.0", "proxyquire": "1.8.0", - "sinon": "3.2.0" + "sinon": "4.1.3" }, "cloud-repo-tools": { "requiresKeyFile": true, - "requiresProjectId": true + "requiresProjectId": true, + "requiredEnvVars": [ + "BASE_URL", + "GCF_REGION", + "FUNCTIONS_CMD" + ] } } diff --git a/functions/datastore/test/index.test.js b/functions/datastore/test/index.test.js index ecbfa01328..a167dbe6ce 100644 --- a/functions/datastore/test/index.test.js +++ b/functions/datastore/test/index.test.js @@ -15,230 +15,173 @@ 'use strict'; -const proxyquire = require(`proxyquire`).noCallThru(); -const sinon = require(`sinon`); const test = require(`ava`); -const tools = require(`@google-cloud/nodejs-repo-tools`); +const Datastore = require(`@google-cloud/datastore`); +const datastore = Datastore(); +const program = require(`../`); +const uuid = require(`uuid`); + +const supertest = require(`supertest`); +const request = supertest(process.env.BASE_URL); const NAME = `sampletask1`; -const KIND = `Task`; +const KIND = `Task-${uuid.v4()}`; const VALUE = { description: `Buy milk` }; -function getSample () { - const key = { - kind: KIND, - name: NAME, - path: [KIND, NAME] - }; - const entity = { - key: key, - data: VALUE - }; - const datastore = { - delete: sinon.stub().returns(Promise.resolve()), - get: sinon.stub().returns(Promise.resolve([entity])), - key: sinon.stub().returns(key), - save: sinon.stub().returns(Promise.resolve()) - }; - const DatastoreMock = sinon.stub().returns(datastore); - - return { - program: proxyquire(`../`, { - '@google-cloud/datastore': DatastoreMock - }), - mocks: { - Datastore: DatastoreMock, - datastore: datastore, - key: key, - entity: entity, - req: { - body: { - kind: KIND, - key: NAME, - value: VALUE - } - }, - res: { - status: sinon.stub().returnsThis(), - send: sinon.stub().returnsThis() - } - } - }; -} - -test.beforeEach(tools.stubConsole); -test.afterEach.always(tools.restoreConsole); - -test.serial(`set: Set fails without a value`, (t) => { - const expectedMsg = `Value not provided. Make sure you have a "value" property in your request`; - const sample = getSample(); +const errorMsg = msg => `${msg} not provided. Make sure you have a "${msg.toLowerCase()}" property in your request`; +test.serial(`set: Fails without a value`, (t) => { + const req = { + body: {} + }; t.throws(() => { - sample.mocks.req.body.value = undefined; - sample.program.set(sample.mocks.req, sample.mocks.res); - }, Error, expectedMsg); + program.set(req, null); + }, errorMsg(`Value`)); }); -test.serial(`set: Set fails without a key`, (t) => { - const expectedMsg = `Key not provided. Make sure you have a "key" property in your request`; - const sample = getSample(); - +test.serial(`set: Fails without a key`, (t) => { + const req = { + body: { + value: VALUE + } + }; t.throws(() => { - sample.mocks.req.body.key = undefined; - sample.program.set(sample.mocks.req, sample.mocks.res); - }, Error, expectedMsg); + program.set(req, null); + }, errorMsg(`Key`)); }); -test.serial(`set: Set fails without a kind`, (t) => { - const expectedMsg = `Kind not provided. Make sure you have a "kind" property in your request`; - const sample = getSample(); - +test.serial(`set: Fails without a kind`, (t) => { + const req = { + body: { + key: NAME, + value: VALUE + } + }; t.throws(() => { - sample.mocks.req.body.kind = undefined; - sample.program.set(sample.mocks.req, sample.mocks.res); - }, Error, expectedMsg); -}); - -test.serial(`set: Handles save error`, async (t) => { - const error = new Error(`error`); - const sample = getSample(); - - sample.mocks.datastore.save.returns(Promise.reject(error)); - - const err = await t.throws(sample.program.set(sample.mocks.req, sample.mocks.res)); - t.deepEqual(err, error); - t.deepEqual(console.error.callCount, 1); - t.deepEqual(console.error.firstCall.args, [error]); - t.deepEqual(sample.mocks.res.status.callCount, 1); - t.deepEqual(sample.mocks.res.status.firstCall.args, [500]); - t.deepEqual(sample.mocks.res.send.callCount, 1); - t.deepEqual(sample.mocks.res.send.firstCall.args, [error]); + program.set(req, null); + }, Error, errorMsg(`Kind`)); }); -test.serial(`set: Set saves an entity`, async (t) => { - const expectedMsg = `Entity ${KIND}/${NAME} saved.`; - const sample = getSample(); - - await sample.program.set(sample.mocks.req, sample.mocks.res); - t.deepEqual(sample.mocks.datastore.save.callCount, 1); - t.deepEqual(sample.mocks.datastore.save.firstCall.args, [sample.mocks.entity]); - t.deepEqual(sample.mocks.res.status.callCount, 1); - t.deepEqual(sample.mocks.res.status.firstCall.args, [200]); - t.deepEqual(sample.mocks.res.send.callCount, 1); - t.deepEqual(sample.mocks.res.send.firstCall.args, [expectedMsg]); +test.serial.cb(`set: Saves an entity`, (t) => { + request + .post(`/set`) + .send({ + kind: KIND, + key: NAME, + value: VALUE + }) + .expect(200) + .expect((response) => { + t.true(response.text.includes(`Entity ${KIND}/${NAME} saved`)); + }) + .end(t.end); }); -test.serial(`get: Get fails without a key`, (t) => { - const expectedMsg = `Key not provided. Make sure you have a "key" property in your request`; - const sample = getSample(); - +test.serial(`get: Fails without a key`, (t) => { + const req = { + body: {} + }; t.throws(() => { - sample.mocks.req.body.key = undefined; - sample.program.get(sample.mocks.req, sample.mocks.res); - }, Error, expectedMsg); + program.get(req, null); + }, Error, errorMsg(`Key`)); }); -test.serial(`get: Get fails without a kind`, (t) => { - const expectedMsg = `Kind not provided. Make sure you have a "kind" property in your request`; - const sample = getSample(); - +test.serial(`get: Fails without a kind`, (t) => { + const req = { + body: { + key: NAME + } + }; t.throws(() => { - sample.mocks.req.body.kind = undefined; - sample.program.get(sample.mocks.req, sample.mocks.res); - }, Error, expectedMsg); + program.get(req, null); + }, Error, errorMsg(`Kind`)); }); -test.serial(`get: Handles get error`, async (t) => { - const error = new Error(`error`); - const sample = getSample(); - - sample.mocks.datastore.get.returns(Promise.reject(error)); - - const err = await t.throws(sample.program.get(sample.mocks.req, sample.mocks.res)); - t.deepEqual(err, error); - t.deepEqual(console.error.callCount, 1); - t.deepEqual(console.error.firstCall.args, [error]); - t.deepEqual(sample.mocks.res.status.callCount, 1); - t.deepEqual(sample.mocks.res.status.firstCall.args, [500]); - t.deepEqual(sample.mocks.res.send.callCount, 1); - t.deepEqual(sample.mocks.res.send.firstCall.args, [error]); +test.serial.cb(`get: Fails when entity does not exist`, (t) => { + request + .post(`/get`) + .send({ + kind: KIND, + key: 'nonexistent' + }) + .expect(500) + .expect((response) => { + t.regex(response.text, /No entity found for key/); + }) + .end(() => { + setTimeout(t.end, 50); // Subsequent test is flaky without this timeout + }); }); -test.serial(`get: Fails when entity does not exist`, async (t) => { - const sample = getSample(); - const error = new Error(`No entity found for key ${sample.mocks.key.path.join('/')}.`); - - sample.mocks.datastore.get.returns(Promise.resolve([])); - - const err = await t.throws(sample.program.get(sample.mocks.req, sample.mocks.res)); - t.deepEqual(err, error); - t.deepEqual(console.error.callCount, 1); - t.deepEqual(console.error.firstCall.args, [error]); - t.deepEqual(sample.mocks.res.status.callCount, 1); - t.deepEqual(sample.mocks.res.status.firstCall.args, [500]); - t.deepEqual(sample.mocks.res.send.callCount, 1); - t.deepEqual(sample.mocks.res.send.firstCall.args, [error]); +test.serial.cb(`get: Finds an entity`, (t) => { + request + .post(`/get`) + .send({ + kind: KIND, + key: NAME + }) + .expect(200) + .expect((response) => { + t.deepEqual( + JSON.parse(response.text), + { description: 'Buy milk' } + ); + }) + .end(t.end); }); -test.serial(`get: Finds an entity`, async (t) => { - const sample = getSample(); - - await sample.program.get(sample.mocks.req, sample.mocks.res); - t.deepEqual(sample.mocks.datastore.get.callCount, 1); - t.deepEqual(sample.mocks.datastore.get.firstCall.args, [sample.mocks.key]); - t.deepEqual(sample.mocks.res.status.callCount, 1); - t.deepEqual(sample.mocks.res.status.firstCall.args, [200]); - t.deepEqual(sample.mocks.res.send.callCount, 1); - t.deepEqual(sample.mocks.res.send.firstCall.args, [sample.mocks.entity]); -}); - -test.serial(`del: Delete fails without a key`, (t) => { - const expectedMsg = `Key not provided. Make sure you have a "key" property in your request`; - const sample = getSample(); - +test.serial(`del: Fails without a key`, (t) => { + const req = { + body: {} + }; t.throws(() => { - sample.mocks.req.body.key = undefined; - sample.program.del(sample.mocks.req, sample.mocks.res); - }, Error, expectedMsg); + program.del(req, null); + }, Error, errorMsg(`Kind`)); }); -test.serial(`del: Delete fails without a kind`, (t) => { - const expectedMsg = `Kind not provided. Make sure you have a "kind" property in your request`; - const sample = getSample(); - +test.serial(`del: Fails without a kind`, (t) => { + const req = { + body: { + key: NAME + } + }; t.throws(() => { - sample.mocks.req.body.kind = undefined; - sample.program.del(sample.mocks.req, sample.mocks.res); - }, Error, expectedMsg); + program.del(req, null); + }, Error, errorMsg(`Kind`)); }); -test.serial(`del: Handles delete error`, async (t) => { - const error = new Error(`error`); - const sample = getSample(); - - sample.mocks.datastore.delete.returns(Promise.reject(error)); - - const err = await t.throws(sample.program.del(sample.mocks.req, sample.mocks.res)); - t.deepEqual(err, error); - t.deepEqual(console.error.callCount, 1); - t.deepEqual(console.error.firstCall.args, [error]); - t.deepEqual(sample.mocks.res.status.callCount, 1); - t.deepEqual(sample.mocks.res.status.firstCall.args, [500]); - t.deepEqual(sample.mocks.res.send.callCount, 1); - t.deepEqual(sample.mocks.res.send.firstCall.args, [error]); +test.serial.cb(`del: Doesn't fail when entity does not exist`, (t) => { + request + .post(`/del`) + .send({ + kind: KIND, + key: 'nonexistent' + }) + .expect(200) + .expect((response) => { + t.is(response.text, `Entity ${KIND}/nonexistent deleted.`); + }) + .end(t.end); }); test.serial(`del: Deletes an entity`, async (t) => { - const expectedMsg = `Entity ${KIND}/${NAME} deleted.`; - const sample = getSample(); - - await sample.program.del(sample.mocks.req, sample.mocks.res); - t.deepEqual(sample.mocks.datastore.delete.callCount, 1); - t.deepEqual(sample.mocks.datastore.delete.firstCall.args, [sample.mocks.key]); - t.deepEqual(sample.mocks.res.status.callCount, 1); - t.deepEqual(sample.mocks.res.status.firstCall.args, [200]); - t.deepEqual(sample.mocks.res.send.callCount, 1); - t.deepEqual(sample.mocks.res.send.firstCall.args, [expectedMsg]); + await new Promise(resolve => { + request + .post(`/del`) + .send({ + kind: KIND, + key: NAME + }) + .expect(200) + .expect((response) => { + t.is(response.text, `Entity ${KIND}/${NAME} deleted.`); + }) + .end(resolve); + }).then(async () => { + const key = datastore.key([KIND, NAME]); + const [entity] = await datastore.get(key); + t.falsy(entity); + }); }); diff --git a/functions/datastore/test/updateFunctions.sh b/functions/datastore/test/updateFunctions.sh new file mode 100644 index 0000000000..df89237334 --- /dev/null +++ b/functions/datastore/test/updateFunctions.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Shell script to emulate/deploy all Cloud Functions in the file + +${FUNCTIONS_CMD} deploy set --trigger-http +echo '-----------------------------' +${FUNCTIONS_CMD} deploy get --trigger-http +echo '-----------------------------' +${FUNCTIONS_CMD} deploy del --trigger-http diff --git a/functions/errorreporting/package.json b/functions/errorreporting/package.json index 9e20871d77..abfd6151bc 100644 --- a/functions/errorreporting/package.json +++ b/functions/errorreporting/package.json @@ -6,6 +6,6 @@ "author": "Google Inc.", "main": "./index.js", "dependencies": { - "@google-cloud/logging": "1.0.2" + "@google-cloud/logging": "1.1.1" } } diff --git a/functions/gcs/package.json b/functions/gcs/package.json index 9b137d6d6e..7eb33730c6 100644 --- a/functions/gcs/package.json +++ b/functions/gcs/package.json @@ -17,14 +17,14 @@ "test": "ava -T 20s --verbose test/*.test.js" }, "dependencies": { - "@google-cloud/storage": "1.2.1", - "request": "2.81.0" + "@google-cloud/storage": "1.4.0", + "request": "2.83.0" }, "devDependencies": { - "@google-cloud/nodejs-repo-tools": "1.4.17", - "ava": "0.21.0", + "@google-cloud/nodejs-repo-tools": "2.1.0", + "ava": "0.23.0", "proxyquire": "1.8.0", - "sinon": "3.2.0" + "sinon": "4.0.2" }, "cloud-repo-tools": { "requiresKeyFile": true, diff --git a/functions/helloworld/index.js b/functions/helloworld/index.js index 4b92064127..b9101d4b5b 100644 --- a/functions/helloworld/index.js +++ b/functions/helloworld/index.js @@ -17,23 +17,6 @@ const Buffer = require('safe-buffer').Buffer; -// [START functions_helloworld_debug] -require('@google-cloud/debug-agent').start(); -// [END functions_helloworld_debug] - -// [START functions_helloworld] -/** - * Cloud Function. - * - * @param {object} event The Cloud Functions event. - * @param {function} callback The callback function. - */ -exports.helloWorld = (event, callback) => { - console.log(`My Cloud Function: ${event.data.message}`); - callback(); -}; -// [END functions_helloworld] - // [START functions_helloworld_get] /** * HTTP Cloud Function. diff --git a/functions/helloworld/package.json b/functions/helloworld/package.json index 4a896d4ba2..42ffb0c51c 100644 --- a/functions/helloworld/package.json +++ b/functions/helloworld/package.json @@ -12,23 +12,37 @@ "node": ">=4.3.2" }, "scripts": { - "lint": "samples lint", + "lint": "repo-tools lint", "pretest": "npm run lint", - "test": "ava -T 20s --verbose test/*.test.js" + "e2e-test": "export FUNCTIONS_CMD='gcloud beta functions' && sh test/updateFunctions.sh && BASE_URL=\"https://$GCF_REGION-$GCLOUD_PROJECT.cloudfunctions.net/\" ava -T 20s --verbose test/*.test.js", + "system-test": "export FUNCTIONS_CMD='functions' && sh test/updateFunctions.sh && export BASE_URL=\"http://localhost:8010/$GCLOUD_PROJECT/$GCF_REGION\" && ava -T 20s --verbose test/*.test.js", + "test": "npm run system-test" }, "dependencies": { - "@google-cloud/debug-agent": "2.1.3", - "pug": "2.0.0-rc.3", + "@google-cloud/debug-agent": "2.3.0", + "pug": "2.0.0-rc.4", "safe-buffer": "5.1.1" }, "devDependencies": { - "@google-cloud/nodejs-repo-tools": "1.4.17", - "ava": "0.21.0", + "@google-cloud/functions-emulator": "^1.0.0-alpha.29", + "@google-cloud/nodejs-repo-tools": "2.1.3", + "@google-cloud/pubsub": "^0.15.0", + "@google-cloud/storage": "^1.5.0", + "ava": "0.24.0", "proxyquire": "1.8.0", - "sinon": "3.2.0" + "sinon": "4.1.2", + "supertest": "^3.0.0", + "uuid": "^3.1.0" }, "cloud-repo-tools": { "requiresKeyFile": true, - "requiresProjectId": true + "requiresProjectId": true, + "requiredEnvVars": [ + "BASE_URL", + "GCF_REGION", + "TOPIC", + "BUCKET", + "FUNCTIONS_CMD" + ] } } diff --git a/functions/helloworld/test/index.test.js b/functions/helloworld/test/index.test.js index 66d0bf96ff..5033955c89 100644 --- a/functions/helloworld/test/index.test.js +++ b/functions/helloworld/test/index.test.js @@ -14,214 +14,208 @@ */ const Buffer = require('safe-buffer').Buffer; -const proxyquire = require(`proxyquire`).noCallThru(); -const sinon = require(`sinon`); +const path = require('path'); const test = require(`ava`); const tools = require(`@google-cloud/nodejs-repo-tools`); +const supertest = require(`supertest`); +const uuid = require(`uuid`); -const program = proxyquire(`../`, { - '@google-cloud/debug-agent': { - start: sinon.stub() - } -}); +const pubsub = require(`@google-cloud/pubsub`)(); +const storage = require(`@google-cloud/storage`)(); + +const baseCmd = process.env.FUNCTIONS_CMD; +const topicName = process.env.FUNCTIONS_TOPIC; -test.beforeEach(tools.stubConsole); -test.afterEach.always(tools.restoreConsole); +const localFileName = `test.txt`; +const fileName = `test-${uuid.v4()}.txt`; -test.serial(`helloworld: should log a message`, (t) => { - const expectedMsg = `My Cloud Function: hi`; - const callback = sinon.stub(); +const BASE_URL = process.env.BASE_URL; - program.helloWorld({ - data: { - message: `hi` - } - }, callback); +const bucketName = process.env.FUNCTIONS_BUCKET; +const bucket = storage.bucket(bucketName); - t.deepEqual(console.log.callCount, 1); - t.deepEqual(console.log.firstCall.args, [expectedMsg]); - t.deepEqual(callback.callCount, 1); - t.deepEqual(callback.firstCall.args, []); +test.before(`Must specify BASE_URL`, t => { + t.truthy(BASE_URL); }); -test.cb.serial(`helloGET: should print hello world`, (t) => { - const expectedMsg = `Hello World!`; +test.before(tools.checkCredentials); - program.helloGET({}, { - send: (message) => { - t.is(message, expectedMsg); - t.end(); - } - }); +test.cb(`helloGET: should print hello world`, (t) => { + supertest(BASE_URL) + .get(`/helloGET`) + .expect(200) + .expect((response) => { + t.is(response.text, `Hello World!`); + }) + .end(t.end); }); -test.cb.serial(`helloHttp: should print a name`, (t) => { - const expectedMsg = `Hello John!`; - - program.helloHttp({ - body: { - name: `John` - } - }, { - send: (message) => { - t.is(message, expectedMsg); - t.end(); - } - }); +test.cb(`helloHttp: should print a name`, (t) => { + supertest(BASE_URL) + .post(`/helloHttp`) + .send({ name: 'John' }) + .expect(200) + .expect((response) => { + t.is(response.text, 'Hello John!'); + }) + .end(t.end); }); -test.cb.serial(`helloHttp: should print hello world`, (t) => { - const expectedMsg = `Hello World!`; - - program.helloHttp({ - body: {} - }, { - send: (message) => { - t.is(message, expectedMsg); - t.end(); - } - }); +test.cb(`helloHttp: should print hello world`, (t) => { + supertest(BASE_URL) + .get(`/helloHttp`) + .expect(200) + .expect((response) => { + t.is(response.text, `Hello World!`); + }) + .end(t.end); }); -test.serial(`helloBackground: should print a name`, (t) => { - const expectedMsg = `Hello John!`; - const callback = sinon.stub(); +test(`helloBackground: should print a name`, async (t) => { + const data = JSON.stringify({name: 'John'}); + const output = await tools.runAsync(`${baseCmd} call helloBackground --data '${data}'`); - program.helloBackground({ - data: { - name: `John` - } - }, callback); + t.true(output.includes('Hello John!')); +}); + +test(`helloBackground: should print hello world`, async (t) => { + const output = await tools.runAsync(`${baseCmd} call helloBackground --data '{}'`); - t.deepEqual(callback.callCount, 1); - t.deepEqual(callback.firstCall.args, [null, expectedMsg]); + t.true(output.includes('Hello World!')); }); -test.serial(`helloBackground: should print hello world`, (t) => { - const expectedMsg = `Hello World!`; - const callback = sinon.stub(); +test(`helloPubSub: should print a name`, async (t) => { + t.plan(0); + const startTime = new Date(Date.now()).toISOString(); + const name = uuid.v4(); - program.helloBackground({ data: {} }, callback); + // Publish to pub/sub topic + const topic = pubsub.topic(topicName); + const publisher = topic.publisher(); + await publisher.publish(Buffer.from(name)); - t.deepEqual(callback.callCount, 1); - t.deepEqual(callback.firstCall.args, [null, expectedMsg]); + // Check logs + await tools.tryTest(async (assert) => { + const logs = await tools.runAsync(`${baseCmd} logs read helloPubSub --start-time ${startTime}`); + assert(logs.includes(`Hello, ${name}!`)); + }); }); -test.serial(`helloPubSub: should print a name`, (t) => { - const expectedMsg = `Hello, Bob!`; - const callback = sinon.stub(); +test(`helloPubSub: should print hello world`, async (t) => { + t.plan(0); + const startTime = new Date(Date.now()).toISOString(); - program.helloPubSub({ - data: { - data: Buffer.from(`Bob`).toString(`base64`) - } - }, callback); + // Publish to pub/sub topic + const topic = pubsub.topic(topicName); + const publisher = topic.publisher(); + await publisher.publish(Buffer.from(''), { a: 'b' }); - t.deepEqual(console.log.callCount, 1); - t.deepEqual(console.log.firstCall.args, [expectedMsg]); - t.deepEqual(callback.callCount, 1); - t.deepEqual(callback.firstCall.args, []); + // Check logs + await tools.tryTest(async (assert) => { + const logs = await tools.runAsync(`${baseCmd} logs read helloPubSub --start-time ${startTime}`); + assert(logs.includes('Hello, World!')); + }); }); -test.serial(`helloPubSub: should print hello world`, (t) => { - const expectedMsg = `Hello, World!`; - const callback = sinon.stub(); +test.serial(`helloGCS: should print uploaded message`, async (t) => { + t.plan(0); + const startTime = new Date(Date.now()).toISOString(); - program.helloPubSub({ data: {} }, callback); + // Upload file + const filepath = path.join(__dirname, localFileName); + await bucket.upload(filepath, { + destination: fileName + }); - t.deepEqual(console.log.callCount, 1); - t.deepEqual(console.log.firstCall.args, [expectedMsg]); - t.deepEqual(callback.callCount, 1); - t.deepEqual(callback.firstCall.args, []); + // Check logs + await tools.tryTest(async (assert) => { + const logs = await tools.runAsync(`${baseCmd} logs read helloPubSub --start-time ${startTime}`); + assert(logs.includes(`File ${fileName} uploaded`)); + }); }); -test.serial(`helloGCS: should print uploaded message`, (t) => { - const expectedMsg = `File foo uploaded.`; - const callback = sinon.stub(); - - program.helloGCS({ - data: { - name: `foo`, - resourceState: `exists`, - metageneration: `1` - } - }, callback); - - t.deepEqual(console.log.callCount, 1); - t.deepEqual(console.log.firstCall.args, [expectedMsg]); - t.deepEqual(callback.callCount, 1); - t.deepEqual(callback.firstCall.args, []); -}); +test.serial(`helloGCS: should print metadata updated message`, async (t) => { + t.plan(0); + const startTime = new Date(Date.now()).toISOString(); -test.serial(`helloGCS: should print metadata updated message`, (t) => { - const expectedMsg = `File foo metadata updated.`; - const callback = sinon.stub(); - - program.helloGCS({ - data: { - name: `foo`, - resourceState: `exists`, - metageneration: `2` - } - }, callback); - - t.deepEqual(console.log.callCount, 1); - t.deepEqual(console.log.firstCall.args, [expectedMsg]); - t.deepEqual(callback.callCount, 1); - t.deepEqual(callback.firstCall.args, []); -}); + // Update file metadata + await bucket.setMetadata(fileName, { foo: `bar` }); -test.serial(`helloGCS: should print deleted message`, (t) => { - const expectedMsg = `File foo deleted.`; - const callback = sinon.stub(); - - program.helloGCS({ - data: { - name: `foo`, - resourceState: `not_exists` - } - }, callback); - - t.deepEqual(console.log.callCount, 1); - t.deepEqual(console.log.firstCall.args, [expectedMsg]); - t.deepEqual(callback.callCount, 1); - t.deepEqual(callback.firstCall.args, []); + // Check logs + await tools.tryTest(async (assert) => { + const logs = await tools.runAsync(`${baseCmd} logs read helloPubSub --start-time ${startTime}`); + assert(logs.includes(`File ${fileName} metadata updated`)); + }); }); -test.serial(`helloError: should throw an error`, (t) => { - const expectedMsg = `I failed you`; +test.serial(`helloGCS: should print deleted message`, async (t) => { + t.plan(0); + const startTime = new Date(Date.now()).toISOString(); - t.throws(() => { - program.helloError(); - }, Error, expectedMsg); + // Delete file + bucket.deleteFiles(); + + // Check logs + await tools.tryTest(async (assert) => { + const logs = await tools.runAsync(`${baseCmd} logs read helloPubSub --start-time ${startTime}`); + assert(logs.includes(`File ${fileName} deleted`)); + }); }); -test.serial(`helloError2: should throw a value`, (t) => { - t.throws(() => { - program.helloError2(); +test(`helloError: should throw an error`, async (t) => { + t.plan(0); + const startTime = new Date(Date.now()).toISOString(); + + // Publish to pub/sub topic + const topic = pubsub.topic(topicName); + const publisher = topic.publisher(); + await publisher.publish(Buffer.from(''), { a: 'b' }); + + // Check logs + await tools.tryTest(async (assert) => { + const logs = await tools.runAsync(`${baseCmd} logs read helloError --start-time ${startTime}`); + assert(logs.includes('Error: I failed you')); }); }); -test.serial(`helloError3: callback shoud return an errback value`, (t) => { - const expectedMsg = `I failed you`; - const callback = sinon.stub(); +test(`helloError2: should throw a value`, async (t) => { + t.plan(0); + const startTime = new Date(Date.now()).toISOString(); - program.helloError3({}, callback); + // Publish to pub/sub topic + const topic = pubsub.topic(topicName); + const publisher = topic.publisher(); + await publisher.publish(Buffer.from(''), { a: 'b' }); - t.deepEqual(callback.callCount, 1); - t.deepEqual(callback.firstCall.args, [expectedMsg]); + // Check logs + await tools.tryTest(async (assert) => { + const logs = await tools.runAsync(`${baseCmd} logs read helloError2 --start-time ${startTime}`); + assert(logs.includes(' 1\n')); + }); }); -test.serial(`helloTemplate: should render the html`, async (t) => { - const req = {}; - const res = {}; - res.send = sinon.stub().returnsThis(); - res.end = sinon.stub().returnsThis(); +test(`helloError3: callback should return an errback value`, async (t) => { + t.plan(0); + const startTime = new Date(Date.now()).toISOString(); + + // Publish to pub/sub topic + const topic = pubsub.topic(topicName); + const publisher = topic.publisher(); + await publisher.publish(Buffer.from(''), { a: 'b' }); - program.helloTemplate(req, res); + // Check logs + await tools.tryTest(async (assert) => { + const logs = await tools.runAsync(`${baseCmd} logs read helloError3 --start-time ${startTime}`); + assert(logs.includes(' I failed you\n')); + }); +}); - t.is(res.send.callCount, 1); - t.is(res.send.getCall(0).args.length, 1); - t.is(res.send.getCall(0).args[0].includes('