├── test ├── .eslintrc.yml ├── unit.test.js ├── debug-errors.test.js ├── http-client.test.js ├── integration-nock.test.js ├── sinon.test.js └── integration-express.test.js ├── .travis.yml ├── README.md ├── simple-worker.js ├── .eslintrc.yml ├── upstream-worker.js ├── worker-debug-errors.js ├── package.json ├── LICENSE └── .gitignore /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | mocha: true 4 | rules: 5 | func-names: [off] 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | 4 | node_js: 5 | - 11 6 | 7 | jobs: 8 | include: 9 | - name: Test 10 | script: 11 | - npm test 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Image](https://i.imgur.com/g3AIIWs.png) 2 | 3 | # Testing Cloudflare workers 4 | 5 | Examples from the blog post on how to [test Cloudflare 6 | workers](https://findwork.dev/blog/testing-cloudflare-workers/). 7 | -------------------------------------------------------------------------------- /simple-worker.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(event) { 2 | const body = {message: 'Hello mocha!'}; 3 | return new Response(JSON.stringify(body), { status: 200 }) 4 | } 5 | 6 | // eslint-disable-next-line no-restricted-globals 7 | addEventListener('fetch', event => { 8 | event.respondWith(handleRequest(event)); 9 | }); 10 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | browser: true 4 | es6: true 5 | extends: 6 | - airbnb-base 7 | - prettier 8 | plugins: 9 | - prettier 10 | globals: 11 | Atomics: readonly 12 | SharedArrayBuffer: readonly 13 | parserOptions: 14 | ecmaVersion: 2018 15 | sourceType: module 16 | rules: 17 | arrow-parens: [error, 'as-needed'] 18 | no-restricted-syntax: [off] 19 | -------------------------------------------------------------------------------- /upstream-worker.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(event) { 2 | const { request } = event; 3 | // Fetch the response from the backend 4 | const response = await fetch(request); 5 | // The response is originally immutable, so we have to clone it in order to 6 | // set the cache headers. 7 | // https://community.cloudflare.com/t/how-can-we-remove-cookies-from-request-to-avoid-being-sent-to-origin/35239/2 8 | const newResponse = new Response(response.body, response); 9 | newResponse.headers.set('my-header', 'some token'); 10 | return newResponse; 11 | } 12 | 13 | // eslint-disable-next-line no-restricted-globals 14 | addEventListener('fetch', event => { 15 | event.respondWith(handleRequest(event)); 16 | }); 17 | -------------------------------------------------------------------------------- /test/unit.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Cloudworker = require('@dollarshaveclub/cloudworker'); 4 | const { expect } = require('chai'); 5 | 6 | const workerScript = fs.readFileSync(path.resolve(__dirname, '../simple-worker.js'), 'utf8'); 7 | 8 | describe('unit test', function () { 9 | this.timeout(60000); 10 | let worker; 11 | 12 | beforeEach(() => { 13 | worker = new Cloudworker(workerScript); 14 | }); 15 | 16 | it('uses worker.dispatch() to test requests', async () => { 17 | const req = new Cloudworker.Request('https://mysite.com/api') 18 | const res = await worker.dispatch(req); 19 | const body = await res.json(); 20 | expect(body).to.eql({message: 'Hello mocha!'}); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/debug-errors.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Cloudworker = require('@dollarshaveclub/cloudworker'); 4 | 5 | const workerScript = fs.readFileSync(path.resolve(__dirname, '../worker-debug-errors.js'), 'utf8'); 6 | 7 | describe('unit test', function () { 8 | this.timeout(60000); 9 | let worker; 10 | 11 | beforeEach(() => { 12 | worker = new Cloudworker(workerScript, { 13 | bindings: { 14 | // Add a global variable that enables error debugging 15 | DEBUG_ERRORS: true, 16 | } 17 | } 18 | ); 19 | }); 20 | 21 | it('uses worker.dispatch() to test requests', async () => { 22 | const req = new Cloudworker.Request('https://mysite.com/api') 23 | await worker.dispatch(req); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /worker-debug-errors.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(event) { 2 | const body = {message: 'Hello mocha!'}; 3 | return new Response(JSON.stringify(body), { status: 200 }) 4 | } 5 | 6 | // A wrapper function which only debugs errors the DEBUG_ERRORS variable is set 7 | async function handle(event) { 8 | // If we're in production the DEBUG_ERRORS variable will not be set 9 | if (typeof DEBUG_ERRORS === 'undefined' || !DEBUG_ERRORS) { 10 | return handleRequest(event); 11 | } 12 | 13 | // Debug crashes in test and development 14 | try { 15 | const res = await handleRequest(event); 16 | return res; 17 | } catch(err) { 18 | console.log(err); 19 | debugger; 20 | } 21 | } 22 | 23 | // eslint-disable-next-line no-restricted-globals 24 | addEventListener('fetch', event => { 25 | event.respondWith(handle(event)); 26 | }); 27 | -------------------------------------------------------------------------------- /test/http-client.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Cloudworker = require('@dollarshaveclub/cloudworker'); 4 | const { expect } = require('chai'); 5 | const axios = require('axios'); 6 | 7 | const workerScript = fs.readFileSync(path.resolve(__dirname, '../simple-worker.js'), 'utf8'); 8 | 9 | describe('http client test', function () { 10 | this.timeout(60000); 11 | let serverAddress; 12 | 13 | beforeEach(() => { 14 | const worker = new Cloudworker(workerScript); 15 | const server = worker.listen(); 16 | serverAddress = `http://localhost:${server.address().port}` 17 | }); 18 | 19 | it('uses axios', async () => { 20 | const response = await axios.get(serverAddress); 21 | expect(response.status).to.eql(200); 22 | expect(response.data).to.eql({message: 'Hello mocha!'}); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/integration-nock.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Cloudworker = require('@dollarshaveclub/cloudworker'); 4 | const { expect } = require('chai'); 5 | const nock = require('nock'); 6 | 7 | const workerScript = fs.readFileSync(path.resolve(__dirname, '../upstream-worker.js'), 'utf8'); 8 | 9 | describe('upstream server test', function () { 10 | this.timeout(60000); 11 | let worker; 12 | 13 | beforeEach(() => { 14 | worker = new Cloudworker(workerScript); 15 | }); 16 | 17 | it('uses Nock upstream server', async () => { 18 | const url = 'http://my-api.test'; 19 | nock(url) 20 | .get('/') 21 | .reply(200, {message: 'Hello from Nock!'}); 22 | 23 | const request = new Cloudworker.Request(url); 24 | const response = await worker.dispatch(request); 25 | const body = await response.json(); 26 | expect(body).to.eql({message: 'Hello from Nock!'}); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-testing-cloudflare-workers", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha 'test/**/*.test.js'" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@dollarshaveclub/cloudworker": "github:dollarshaveclub/cloudworker", 17 | "@dollarshaveclub/node-fetch": "^3.2.0", 18 | "axios": "^0.19.0", 19 | "chai": "^4.2.0", 20 | "eslint": "^6.1.0", 21 | "eslint-config-airbnb": "^18.0.1", 22 | "eslint-config-prettier": "^6.4.0", 23 | "eslint-plugin-import": "^2.18.2", 24 | "eslint-plugin-jsx-a11y": "^6.2.3", 25 | "eslint-plugin-react": "^7.14.3", 26 | "eslint-plugin-react-hooks": "^1.7.0", 27 | "form-data": "^2.5.1", 28 | "mocha": "^6.2.1", 29 | "ndb": "^1.1.5", 30 | "nock": "^11.3.5", 31 | "sinon": "^7.5.0", 32 | "eslint-plugin-prettier": "^3.1.1", 33 | "express": "^4.17.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dani Hodovic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/sinon.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Cloudworker = require('@dollarshaveclub/cloudworker'); 4 | const fetch = require('@dollarshaveclub/node-fetch'); 5 | const sinon = require('sinon'); 6 | const nock = require('nock'); 7 | 8 | const workerScript = fs.readFileSync(path.resolve(__dirname, '../upstream-worker.js'), 'utf8'); 9 | 10 | describe('unit test', function () { 11 | this.timeout(60000); 12 | let worker; 13 | let fetchMock; 14 | 15 | beforeEach(() => { 16 | fetchMock = sinon.fake(fetch); 17 | worker = new Cloudworker(workerScript, { 18 | // Inject our mocked fetch into the worker script 19 | bindings: { 20 | fetch: fetchMock 21 | } 22 | } 23 | ); 24 | }); 25 | 26 | it('uses Sinon.js spies to assert calls', async () => { 27 | const url = 'http://my-api.test'; 28 | nock(url) 29 | .get('/') 30 | .reply(200, {message: 'Hello from Nock!'}); 31 | 32 | const request = new Cloudworker.Request(url) 33 | await worker.dispatch(request); 34 | 35 | const expected = new Cloudworker.Request(url); 36 | sinon.assert.calledWith(fetchMock, expected); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/integration-express.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Cloudworker = require('@dollarshaveclub/cloudworker'); 4 | const { expect } = require('chai'); 5 | const express = require('express'); 6 | 7 | const workerScript = fs.readFileSync(path.resolve(__dirname, '../upstream-worker.js'), 'utf8'); 8 | 9 | function createApp() { 10 | const app = express(); 11 | app.get('/', function (request, response) { 12 | response.json({message: 'Hello from express!'}); 13 | }); 14 | return app; 15 | } 16 | 17 | describe('upstream server test', function () { 18 | this.timeout(60000); 19 | let serverAddress; 20 | let worker; 21 | 22 | beforeEach(() => { 23 | const upstream = createApp().listen(); 24 | serverAddress = `http://localhost:${upstream.address().port}` 25 | worker = new Cloudworker(workerScript); 26 | }); 27 | 28 | it('uses Express upstream server', async () => { 29 | const request = new Cloudworker.Request(serverAddress); 30 | const response = await worker.dispatch(request); 31 | const body = await response.json(); 32 | expect(response.headers.get('my-header')).to.eql('some token'); 33 | expect(body).to.eql({message: 'Hello from express!'}); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # next.js build output 78 | .next 79 | 80 | # nuxt.js build output 81 | .nuxt 82 | 83 | # react / gatsby 84 | public/ 85 | 86 | # vuepress build output 87 | .vuepress/dist 88 | 89 | # Serverless directories 90 | .serverless/ 91 | 92 | # FuseBox cache 93 | .fusebox/ 94 | 95 | # DynamoDB Local files 96 | .dynamodb/ 97 | 98 | # End of https://www.gitignore.io/api/node 99 | --------------------------------------------------------------------------------