diff --git a/circle.yml b/circle.yml index e98ee2eea5..93790d7600 100644 --- a/circle.yml +++ b/circle.yml @@ -18,7 +18,7 @@ machine: node: - version: 6.12.3 + version: 6.14.1 # Use for broader build-related configuration general: @@ -35,10 +35,12 @@ dependencies: - yarn install - yarn run lint - samples test install -l=functions/background + - samples test install -l=functions/concepts + - 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 @@ -46,10 +48,12 @@ dependencies: - samples test install -l=functions/sendgrid - samples test install -l=functions/slack - samples test install -l=functions/spanner + - samples test install -l=functions/tips - samples test install -l=functions/uuid cache_directories: - ~/.cache/yarn - functions/background/node_modules + - functions/concepts/node_modules - functions/datastore/node_modules - functions/errorreporting/node_modules - functions/gcs/node_modules @@ -62,6 +66,7 @@ dependencies: - functions/sendgrid/node_modules - functions/slack/node_modules - functions/spanner/node_modules + - functions/tips/node_modules - functions/uuid/node_modules # Run your tests @@ -70,6 +75,7 @@ test: - functions-emulator config set projectId $GCLOUD_PROJECT && functions-emulator start && cd functions/datastore && npm run system-test && functions-emulator stop - functions-emulator config set projectId $GCLOUD_PROJECT && functions-emulator start && cd functions/helloworld && npm run test && functions-emulator stop - 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/concepts/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' @@ -79,6 +85,7 @@ test: - 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' + - samples test run --cmd nyc -- --cache ava --verbose -T 30s 'functions/tips/test/**/*.test.js' post: - nyc report --reporter=lcov > coverage.lcov && codecov || true deployment: diff --git a/functions/concepts/index.js b/functions/concepts/index.js new file mode 100644 index 0000000000..6800aff3fb --- /dev/null +++ b/functions/concepts/index.js @@ -0,0 +1,175 @@ +/** + * Copyright 2018, 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'; + +/** + * HTTP Cloud Function that demonstrates + * how to catch errors of different types. + * + * @param {Object} req Cloud Function request context. + * @param {Object} req.body Cloud Function request context body. + * @param {String} req.body.topic The Cloud Pub/Sub topic to publish to. + * @param {Object} res Cloud Function response context. + */ +exports.errorTypes = (req, res) => { + // [START functions_concepts_error_object] + try { + // Throw an Error object (to simulate a GCP API failure) + throw new Error('Error object!'); + } catch (err) { + // err is already an Error object + console.error(err); + } + // [END functions_concepts_error_object] + + const someCondition = !!req.body.throwAsString; + + /* eslint-disable no-throw-literal */ + // [START functions_concepts_error_unknown] + try { + // Throw an unknown error type + if (someCondition) { + throw 'Error string!'; + } else { + throw new Error('Error object!'); + } + } catch (err) { + // Determine the error type + if (err instanceof Error) { + console.error(err); + } else { + console.error(new Error(err)); + } + } + // [END functions_concepts_error_unknown] + /* eslint-enable no-throw-literal */ + + res.end(); +}; + +// [START functions_concepts_stateless] +// Global variable, but only shared within function instance. +let count = 0; + +/** + * HTTP Cloud Function that counts how many times + * it is executed within a specific instance. + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.executionCount = (req, res) => { + count++; + + // Note: the total function invocation count across + // all instances may not be equal to this value! + res.send(`Instance execution count: ${count}`); +}; +// [END functions_concepts_stateless] + +// [START functions_concepts_after_response] +/** + * HTTP Cloud Function that may not completely + * execute due to early HTTP response + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.afterResponse = (req, res) => { + res.end(); + + // This statement may not execute + console.log('Function complete!'); +}; +// [END functions_concepts_after_response] + +// [START functions_concepts_after_timeout] +/** + * HTTP Cloud Function that may not completely + * execute due to function execution timeout + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.afterTimeout = (req, res) => { + setTimeout(() => { + // May not execute if function's timeout is <2 minutes + console.log('Function running...'); + res.end(); + }, 120000); // 2 minute delay +}; +// [END functions_concepts_after_timeout] + +// [START functions_concepts_filesystem] +const fs = require('fs'); + +/** + * HTTP Cloud Function that lists files in the function directory + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.listFiles = (req, res) => { + fs.readdir(__dirname, (err, files) => { + if (err) { + console.error(err); + res.sendStatus(500); + } else { + console.log('Files', files); + res.sendStatus(200); + } + }); +}; +// [END functions_concepts_filesystem] + +// [START functions_concepts_modules] +const path = require('path'); +const loadedModule = require(path.join(__dirname, 'loadable.js')); + +/** + * HTTP Cloud Function that runs a function loaded from another Node.js file + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.runLoadedModule = (req, res) => { + console.log(`Loaded function from file ${loadedModule.getFileName()}`); + res.end(); +}; +// [END functions_concepts_modules] + +// [START functions_concepts_requests] +const request = require('request'); + +/** + * HTTP Cloud Function that makes an HTTP request + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.makeRequest = (req, res) => { + // The URL to send the request to + const url = 'https://example.com'; + + request(url, (err, response) => { + if (!err && response.statusCode === 200) { + res.sendStatus(200); + } else { + res.sendStatus(500); + } + }); +}; +// [END functions_concepts_requests] diff --git a/functions/concepts/loadable.js b/functions/concepts/loadable.js new file mode 100644 index 0000000000..822b444d20 --- /dev/null +++ b/functions/concepts/loadable.js @@ -0,0 +1,22 @@ +/** + * Copyright 2018, 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 functions_sample_module] +exports.getFileName = () => { + return __filename; +}; +// [END functions_sample_module] diff --git a/functions/concepts/package.json b/functions/concepts/package.json new file mode 100644 index 0000000000..298ba762f5 --- /dev/null +++ b/functions/concepts/package.json @@ -0,0 +1,26 @@ +{ + "name": "nodejs-docs-samples-functions-concepts", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google Inc.", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": ">=4.3.2" + }, + "scripts": { + "lint": "repo-tools lint" + }, + "dependencies": { + "request": "^2.85.0" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "^2.2.5", + "ava": "^0.25.0", + "sinon": "^4.5.0", + "supertest": "^3.0.0" + } +} diff --git a/functions/concepts/test/index.test.js b/functions/concepts/test/index.test.js new file mode 100644 index 0000000000..f0ffaf4129 --- /dev/null +++ b/functions/concepts/test/index.test.js @@ -0,0 +1,46 @@ +/** + * Copyright 2018, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const sinon = require(`sinon`); +const test = require(`ava`); +const tools = require(`@google-cloud/nodejs-repo-tools`); + +const sample = require(`../`); + +test.beforeEach(tools.stubConsole); +test.afterEach.always(tools.restoreConsole); + +test(`should demonstrate error type behavior`, (t) => { + const objError = new Error('Error object!'); + const strError = new Error('Error string!'); + + const req = { body: + { throwAsString: true } + }; + const res = { end: sinon.stub() }; + + // Test throwing both objects and strings + sample.errorTypes(req, res); + t.deepEqual(console.error.getCall(0).args, [objError]); + t.deepEqual(console.error.getCall(1).args, [strError]); + + // Test throwing objects only + req.body.throwAsString = false; + sample.errorTypes(req, res); + t.deepEqual(console.error.getCall(2).args, [objError]); + t.deepEqual(console.error.getCall(3).args, [objError]); +}); diff --git a/functions/helloworld/.gcloudignore b/functions/helloworld/.gcloudignore new file mode 100644 index 0000000000..ccc4eb240e --- /dev/null +++ b/functions/helloworld/.gcloudignore @@ -0,0 +1,16 @@ +# This file specifies files that are *not* uploaded to Google Cloud Platform +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +node_modules diff --git a/functions/helloworld/index.js b/functions/helloworld/index.js index 3eda309167..32ceeb4263 100644 --- a/functions/helloworld/index.js +++ b/functions/helloworld/index.js @@ -1,5 +1,5 @@ /** - * Copyright 2016, Google, Inc. + * Copyright 2018, 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 diff --git a/functions/http/index.js b/functions/http/index.js index 317994e015..13a92e9b13 100644 --- a/functions/http/index.js +++ b/functions/http/index.js @@ -105,3 +105,116 @@ exports.helloHttp = (req, res) => { } }; // [END functions_http_method] + +// [START functions_http_xml] +/** + * Parses a document of type 'text/xml' + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.parseXML = (req, res) => { + // Convert the request to a Buffer and a string + // Use whichever one is accepted by your XML parser + let data = req.rawBody; + let xmlData = data.toString(); + + const parseString = require('xml2js').parseString; + + parseString(xmlData, (err, result) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + res.send(result); + }); +}; +// [END functions_http_xml] + +// [START functions_http_form_data] +/** + * Parses a 'multipart/form-data' upload request + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +const path = require('path'); +const os = require('os'); +const fs = require('fs'); +const Busboy = require('busboy'); + +exports.uploadFile = (req, res) => { + if (req.method === 'POST') { + const busboy = new Busboy({ headers: req.headers }); + + // This object will accumulate all the uploaded files, keyed by their name. + const uploads = {}; + const tmpdir = os.tmpdir(); + + // This callback will be invoked for each file uploaded. + busboy.on('file', (fieldname, file, filename) => { + // Note: os.tmpdir() points to an in-memory file system on GCF + // Thus, any files in it must fit in the instance's memory. + const filepath = path.join(tmpdir, filename); + uploads[fieldname] = filepath; + file.pipe(fs.createWriteStream(filepath)); + }); + + // This callback will be invoked after all uploaded files are saved. + busboy.on('finish', () => { + // TODO(developer): Process uploaded files here + for (const name in uploads) { + const file = uploads[name]; + fs.unlinkSync(file); + } + res.send(); + }); + + req.pipe(busboy); + } else { + // Return a "method not allowed" error + res.status(405).end(); + } +}; +// [END functions_http_form_data] + +// [START functions_http_signed_url] +const storage = require('@google-cloud/storage')(); + +/** + * HTTP function that generates a signed URL + * The signed URL can be used to upload files to Google Cloud Storage (GCS) + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.getSignedUrl = (req, res) => { + if (req.method === 'POST') { + // TODO(developer) check that the user is authorized to upload + + // Get a reference to the destination file in GCS + const file = storage.bucket('my-bucket').file(req.body.filename); + + // Create a temporary upload URL + const expiresAtMs = Date.now() + 300000; // Link expires in 5 minutes + const config = { + action: 'write', + expires: expiresAtMs, + contentType: req.body.contentType + }; + + file.getSignedUrl(config, function (err, url) { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + res.send(url); + }); + } else { + // Return a "method not allowed" error + res.status(405).end(); + } +}; +// [END functions_http_signed_url] diff --git a/functions/http/package.json b/functions/http/package.json index 18638d347b..5a82ee97e2 100644 --- a/functions/http/package.json +++ b/functions/http/package.json @@ -23,6 +23,7 @@ "sinon": "4.4.2" }, "dependencies": { + "busboy": "^0.2.14", "safe-buffer": "5.1.1" }, "cloud-repo-tools": { diff --git a/functions/tips/.gcloudignore b/functions/tips/.gcloudignore new file mode 100644 index 0000000000..ccc4eb240e --- /dev/null +++ b/functions/tips/.gcloudignore @@ -0,0 +1,16 @@ +# This file specifies files that are *not* uploaded to Google Cloud Platform +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +node_modules diff --git a/functions/tips/index.js b/functions/tips/index.js new file mode 100644 index 0000000000..7616ebf844 --- /dev/null +++ b/functions/tips/index.js @@ -0,0 +1,231 @@ +/** + * Copyright 2018, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const lightComputation = () => { + const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + return numbers.reduce((t, x) => t + x); +}; + +const heavyComputation = () => { + // Multiplication is more computationally expensive than addition + const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + return numbers.reduce((t, x) => t * x); +}; + +const functionSpecificComputation = heavyComputation; +const fileWideComputation = lightComputation; + +// [START functions_tips_scopes] +// Global (instance-wide) scope +// This computation runs at instance cold-start +const instanceVar = heavyComputation(); + +/** + * HTTP Cloud Function that declares a variable. + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.scopeDemo = (req, res) => { + // Per-function scope + // This computation runs every time this function is called + const functionVar = lightComputation(); + + res.send(`Per instance: ${instanceVar}, per function: ${functionVar}`); +}; +// [END functions_tips_scopes] + +// [START functions_tips_lazy_globals] +// Always initialized (at cold-start) +const nonLazyGlobal = fileWideComputation(); + +// Declared at cold-start, but only initialized if/when the function executes +let lazyGlobal; + +/** + * HTTP Cloud Function that uses lazy-initialized globals + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.lazyGlobals = (req, res) => { + // This value is initialized only if (and when) the function is called + lazyGlobal = lazyGlobal || functionSpecificComputation(); + + res.send(`Lazy global: ${lazyGlobal}, non-lazy global: ${nonLazyGlobal}`); +}; +// [END functions_tips_lazy_globals] + +// [START functions_tips_ephemeral_agent] +// [START functions_tips_cached_agent] +const http = require('http'); +// [END functions_tips_ephemeral_agent] +const agent = new http.Agent({keepAlive: true}); +// [END functions_tips_cached_agent] + +// [START functions_tips_ephemeral_agent] + +/** + * HTTP Cloud Function that uses an ephemeral HTTP agent + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.ephemeralAgent = (req, res) => { + req = http.request({ + host: '', + port: 80, + path: '', + method: 'GET' + }, resInner => { + let rawData = ''; + resInner.setEncoding('utf8'); + resInner.on('data', chunk => { rawData += chunk; }); + resInner.on('end', () => { + res.status(200).send(`Data: ${rawData}`); + }); + }); + req.on('error', (e) => { + res.status(500).send(`Error: ${e.message}`); + }); + req.end(); +}; +// [END functions_tips_ephemeral_agent] + +// [START functions_tips_cached_agent] +/** + * HTTP Cloud Function that uses a cached HTTP agent + * + * @param {Object} req Cloud Function request context. + * @param {Object} res Cloud Function response context. + */ +exports.cachedAgent = (req, res) => { + req = http.request({ + host: '', + port: 80, + path: '', + method: 'GET', + agent: agent + }, resInner => { + let rawData = ''; + resInner.setEncoding('utf8'); + resInner.on('data', chunk => { rawData += chunk; }); + resInner.on('end', () => { + res.status(200).send(`Data: ${rawData}`); + }); + }); + req.on('error', e => { + res.status(500).send(`Error: ${e.message}`); + }); + req.end(); +}; +// [END functions_tips_cached_agent] + +// [START functions_tips_infinite_retries] +/** + * Background Cloud Function that only executes within + * a certain time period after the triggering event + * + * @param {object} event The Cloud Functions event. + * @param {function} callback The callback function. + */ +exports.avoidInfiniteRetries = (event, callback) => { + const eventAge = Date.now() - Date.parse(event.timestamp); + const eventMaxAge = 10000; + + // Ignore events that are too old + if (eventAge > eventMaxAge) { + console.log(`Dropping event ${event} with age ${eventAge} ms.`); + callback(); + return; + } + + // Do what the function is supposed to do + console.log(`Processing event ${event} with age ${eventAge} ms.`); + callback(); +}; +// [END functions_tips_infinite_retries] + +// [START functions_tips_retry_promise] +/** + * Background Cloud Function that demonstrates + * how to toggle retries using a promise + * + * @param {object} event The Cloud Functions event. + * @param {object} event.data Data included with the event. + * @param {object} event.data.retry Whether or not to retry the function. + */ +exports.retryPromise = (event) => { + const tryAgain = !!event.data.retry; + + if (tryAgain) { + throw new Error(`Retrying...`); + } else { + return Promise.reject(new Error('Not retrying...')); + } +}; +// [END functions_tips_retry_promise] + +// [START functions_tips_retry_callback] +/** + * Background Cloud Function that demonstrates + * how to toggle retries using a callback + * + * @param {object} event The Cloud Functions event. + * @param {object} event.data Data included with the event. + * @param {object} event.data.retry Whether or not to retry the function. + * @param {function} callback The callback function. + */ +exports.retryCallback = (event, callback) => { + const tryAgain = !!event.data.retry; + const err = new Error('Error!'); + + if (tryAgain) { + console.error('Retrying:', err); + callback(err); + } else { + console.error('Not retrying:', err); + callback(); + } +}; +// [END functions_tips_retry_callback] + +// [START functions_tips_gcp_apis] +const Pubsub = require('@google-cloud/pubsub'); +const pubsub = Pubsub(); + +/** + * HTTP Cloud Function that uses a cached client library instance to + * reduce the number of connections required per function invocation. + * + * @param {Object} req Cloud Function request context. + * @param {Object} req.body Cloud Function request context body. + * @param {String} req.body.topic The Cloud Pub/Sub topic to publish to. + * @param {Object} res Cloud Function response context. + */ +exports.gcpApiCall = (req, res) => { + const topic = pubsub.topic(req.body.topic); + + topic.publish('Test message', err => { + if (err) { + res.status(500).send(`Error publishing the message: ${err}`); + } else { + res.status(200).send('1 message published'); + } + }); +}; +// [END functions_tips_gcp_apis] diff --git a/functions/tips/package.json b/functions/tips/package.json new file mode 100644 index 0000000000..e29698ada7 --- /dev/null +++ b/functions/tips/package.json @@ -0,0 +1,38 @@ +{ + "name": "nodejs-docs-samples-functions-tips", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google Inc.", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": ">=6.14.0" + }, + "scripts": { + "lint": "repo-tools lint", + "pretest": "npm run lint" + }, + "dependencies": { + "@google-cloud/pubsub": "^0.18.0", + "safe-buffer": "5.1.1" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "2.2.5", + "ava": "0.25.0", + "sinon": "^4.5.0" + }, + "cloud-repo-tools": { + "requiresKeyFile": true, + "requiresProjectId": true, + "requiredEnvVars": [ + "BASE_URL", + "GCF_REGION", + "TOPIC", + "BUCKET", + "FUNCTIONS_CMD" + ] + } +} diff --git a/functions/tips/test/index.test.js b/functions/tips/test/index.test.js new file mode 100644 index 0000000000..4bca0df209 --- /dev/null +++ b/functions/tips/test/index.test.js @@ -0,0 +1,55 @@ +/** + * Copyright 2018, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const sinon = require(`sinon`); +const test = require(`ava`); +const tools = require(`@google-cloud/nodejs-repo-tools`); + +const sample = require(`../`); + +test.beforeEach(tools.stubConsole); +test.afterEach.always(tools.restoreConsole); + +test(`should demonstrate retry behavior for a promise`, async (t) => { + // Retry by throwing an error + t.throws(() => { + sample.retryPromise({ data: { + retry: true } + }); + }, 'Retrying...'); + + // Terminate by returning a rejected promise + await t.throws( + sample.retryPromise({ data: {} }), + 'Not retrying...' + ); +}); + +test(`should demonstrate retry behavior for a callback`, (t) => { + const cb = sinon.stub(); + const err = new Error('Error!'); + + // Retry by passing an error to the callback + sample.retryCallback({ data: { + retry: true } + }, cb); + t.deepEqual(cb.firstCall.args, [err]); + + // Terminate by passing nothing to the callback + sample.retryCallback({ data: {} }, cb); + t.deepEqual(cb.secondCall.args, []); +});