From 9c5f942309c8ed4a53c94a4740ff053c17d9c131 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Wed, 20 Jul 2016 22:40:00 -0700 Subject: [PATCH] Switch CloudSQL sample to 2nd gen. Added unit tests. (#153) * Switch CloudSQL sample to 2nd gen. Add units tests. * Fix typo. --- appengine/cloudsql/README.md | 93 ++++++++++---- appengine/cloudsql/app.js | 79 ------------ appengine/cloudsql/app.yaml | 19 ++- appengine/cloudsql/createTables.js | 83 ++++++++++++ appengine/cloudsql/create_tables.js | 45 ------- appengine/cloudsql/package.json | 11 +- appengine/cloudsql/server.js | 113 +++++++++++++++++ test/appengine/all.test.js | 4 +- test/appengine/cloudsql/createTables.test.js | 114 +++++++++++++++++ test/appengine/cloudsql/server.test.js | 127 +++++++++++++++++++ 10 files changed, 526 insertions(+), 162 deletions(-) delete mode 100644 appengine/cloudsql/app.js create mode 100644 appengine/cloudsql/createTables.js delete mode 100644 appengine/cloudsql/create_tables.js create mode 100644 appengine/cloudsql/server.js create mode 100644 test/appengine/cloudsql/createTables.test.js create mode 100644 test/appengine/cloudsql/server.test.js diff --git a/appengine/cloudsql/README.md b/appengine/cloudsql/README.md index 87a3fd413f..640cc12b32 100644 --- a/appengine/cloudsql/README.md +++ b/appengine/cloudsql/README.md @@ -1,40 +1,89 @@ # Node.js Cloud SQL sample on Google App Engine -This sample demonstrates how to use [Cloud SQL](https://cloud.google.com/sql/) -on [Google App Engine Managed VMs](https://cloud.google.com/appengine). +This sample demonstrates how to use [Google Cloud SQL][sql] (or any other SQL +server) on [Google App Engine Flexible][flexible]. ## Setup Before you can run or deploy the sample, you will need to do the following: -1. Create a Cloud SQL instance. You can do this from the [Google Developers Console](https://console.developers.google.com) -or via the [Cloud SDK](https://cloud.google.com/sdk). To create it via the SDK -use the following command: +1. Create a [Second Generation Cloud SQL][gen] instance. You can do this from +the [Cloud Console][console] or via the [Cloud SDK][sdk]. To create it via the +SDK use the following command: - gcloud sql instances create [your-instance-name] \ - --assign-ip \ - --authorized-networks 0.0.0.0/0 \ - --tier D0 + gcloud sql instances create [YOUR_INSTANCE_NAME] \ + --activation-policy=ALWAYS \ + --tier=db-n1-standard-1 + + where `[YOUR_INSTANCE_NAME]` is a name of your choice. + +1. Set the root password on your Cloud SQL instance: + + gcloud sql instances set-root-password [YOUR_INSTANCE_NAME] --password [YOUR_INSTANCE_ROOT_PASSWORD] + + where `[YOUR_INSTANCE_NAME]` is the name you chose in step 1 and + `[YOUR_INSTANCE_ROOT_PASSWORD]` is a password of your choice. + +1. Create a [Service Account][service] for your project. You will use this +service account to connect to your Cloud SQL instance locally. + +1. Download and install the [Cloud SQL Proxy][proxy]. + +1. [Start the proxy][start] to allow connecting to your instance from your local +machine: + + cloud_sql_proxy \ + -dir /cloudsql \ + -instances=[YOUR_INSTANCE_CONNECTION_NAME] \ + -credential_file=PATH_TO_YOUR_SERVICE_ACCOUNT_JSON + + where `[YOUR_INSTANCE_CONNECTION_NAME]` is the connection name of your + instance on its Overview page in the Google Cloud Platform Console, or use + `[YOUR_PROJECT_ID]:[YOUR_REGION]:[YOUR_INSTANCE_NAME]`. + +1. Use the MySQL command line tools (or a management tool of your choice) to +create a [new user][user] and [database][database] for your application: + + mysql --socket [YOUR_SOCKET_PATH] -u root -p + mysql> create database YOUR_DATABASE; + mysql> create user 'YOUR_USER'@'%' identified by 'PASSWORD'; + mysql> grant all on YOUR_DATABASE.* to 'YOUR_USER'@'%'; + + where `[YOUR_SOCKET_PATH]` is that socket opened by the proxy. This path was + printed to the console when you started the proxy, and is of the format: + `/[DIR]/[YOUR_PROJECT_ID]:[YOUR_REGION]:[YOUR_INSTANCE_NAME]`. + +1. Set the `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_SOCKET_PATH`, and +`MYSQL_DATABASE` environment variables. This allows the app to connect to your +Cloud SQL instance through the proxy. -1. Create a new user and database for the application. The easiest way to do -this is via the [Google Developers Console](https://console.developers.google.com/project/_/sql/instances/example-instance2/access-control/users). -Alternatively, you can use MySQL tools such as the command line client or -workbench. 1. Update the values in in `app.yaml` with your instance configuration. -1. Finally, run `create_tables.js` to ensure that the database is properly + +1. Finally, run `createTables.js` to ensure that the database is properly configured and to create the tables needed for the sample. ## Running locally -Refer to the [appengine/README.md](../README.md) file for instructions on -running and deploying. +Refer to the [top-level README](../README.md) for instructions on running and deploying. -To run locally, set the environment variables via your shell before running the -sample: +It's recommended to follow the instructions above to run the Cloud SQL proxy. +You will need to set the following environment variables via your shell before +running the sample: - export MYSQL_HOST= - export MYSQL_USER= - export MYSQL_PASSWORD= - export MYSQL_DATABASE= + export MYSQL_USER="YOUR_USER" + export MYSQL_PASSWORD="YOUR_PASSWORD" + export MYSQL_SOCKET_PATH="YOUR_SOCKET_PATH" + export MYSQL_DATABASE="YOUR_DATABASE" npm install npm start + +[sql]: https://cloud.google.com/sql/ +[flexible]: https://cloud.google.com/appengine +[gen]: https://cloud.google.com/sql/docs/create-instance +[console]: https://console.developers.google.com +[sdk]: https://cloud.google.com/sdk +[service]: https://cloud.google.com/sql/docs/external#createServiceAccount +[proxy]: https://cloud.google.com/sql/docs/external#install +[start]: https://cloud.google.com/sql/docs/external#6_start_the_proxy +[user]: https://cloud.google.com/sql/docs/create-user +[database]: https://cloud.google.com/sql/docs/create-database diff --git a/appengine/cloudsql/app.js b/appengine/cloudsql/app.js deleted file mode 100644 index b1b208204f..0000000000 --- a/appengine/cloudsql/app.js +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2015-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. -// [START app] -'use strict'; - -var format = require('util').format; -var express = require('express'); -var mysql = require('mysql'); -var crypto = require('crypto'); - -var app = express(); -app.enable('trust proxy'); - -// These environment variables are set by app.yaml when running on GAE, but -// will need to be manually set when running locally. -// If you're connecting via SSL you will need to specify your certs here, see -// https://github.com/felixge/node-mysql/#ssl-options -var connection = mysql.createConnection({ - host: process.env.MYSQL_HOST, - user: process.env.MYSQL_USER, - password: process.env.MYSQL_PASSWORD, - database: process.env.MYSQL_DATABASE -}); - -app.get('/', function (req, res, next) { - var hash = crypto.createHash('sha256'); - - // Add this visit to the database - var visit = { - timestamp: new Date(), - // Store a hash of the ip address - userIp: hash.update(req.ip).digest('hex').substr(0, 7) - }; - - connection.query('INSERT INTO `visits` SET ?', visit, function (err) { - if (err) { - return next(err); - } - - // Query the last 10 visits from the database. - connection.query( - 'SELECT `timestamp`, `userIp` FROM `visits` ORDER BY `timestamp` DESC ' + - 'LIMIT 10', - function (err, results) { - if (err) { - return next(err); - } - - var visits = results.map(function (visit) { - return format( - 'Time: %s, AddrHash: %s', - visit.timestamp, - visit.userIp); - }); - - var output = format('Last 10 visits:\n%s', visits.join('\n')); - - res.set('Content-Type', 'text/plain'); - res.status(200).send(output); - }); - }); -}); - -var server = app.listen(process.env.PORT || 8080, function () { - console.log('App listening on port %s', server.address().port); - console.log('Press Ctrl+C to quit.'); -}); -// [END app] diff --git a/appengine/cloudsql/app.yaml b/appengine/cloudsql/app.yaml index eea86bf840..18f0e78f47 100644 --- a/appengine/cloudsql/app.yaml +++ b/appengine/cloudsql/app.yaml @@ -17,11 +17,18 @@ vm: true # [START env] env_variables: - # Replace user, password, and host with the values obtained when - # configuring your Cloud SQL instance. - MYSQL_HOST: - MYSQL_USER: - MYSQL_PASSWORD: - MYSQL_DATABASE: + MYSQL_USER: YOUR_USER + MYSQL_PASSWORD: YOUR_PASSWORD + MYSQL_DATABASE: YOUR_DATABASE + # This path was printed to the console when you started the proxy, and is of + # the format: `/[DIR]/[YOUR_PROJECT_ID]:[YOUR_REGION]:[YOUR_INSTANCE_NAME]` + MYSQL_SOCKET_PATH: YOUR_SOCKET_PATH # [END env] + +# [START cloudsql_settings] +beta_settings: + # The connection name of your instance on its Overview page in the Google + # Cloud Platform Console, or use `[YOUR_PROJECT_ID]:[YOUR_REGION]:[YOUR_INSTANCE_NAME]` + cloud_sql_instances: YOUR_INSTANCE_CONNECTION_NAME +# [END cloudsql_settings] # [END app_yaml] diff --git a/appengine/cloudsql/createTables.js b/appengine/cloudsql/createTables.js new file mode 100644 index 0000000000..541841dded --- /dev/null +++ b/appengine/cloudsql/createTables.js @@ -0,0 +1,83 @@ +// Copyright 2015-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. + +// [START createTables] +'use strict'; + +// [START setup] +var mysql = require('mysql'); +var prompt = require('prompt'); +// [END setup] + +// [START createTable] +var SQL_STRING = 'CREATE TABLE `visits` (\n' + + ' `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,\n' + + ' `timestamp` DATETIME NULL,\n' + + ' `userIp` VARCHAR(46) NULL,\n' + + ' PRIMARY KEY (`id`)\n' + + ');'; + +/** + * Create the "visits" table. + * + * @param {object} connection A mysql connection object. + * @param {function} callback The callback function. + */ +function createTable (connection, callback) { + connection.query(SQL_STRING, callback); +} +// [END createTable] + +// [START getConnection] +var FIELDS = ['socketPath', 'user', 'password', 'database']; + +/** + * Ask the user for connection configuration and create a new connection. + * + * @param {function} callback The callback function. + */ +function getConnection (callback) { + prompt.start(); + prompt.get(FIELDS, function (err, config) { + if (err) { + return callback(err); + } + + return callback(null, mysql.createConnection({ + user: config.user, + password: config.password, + socketPath: config.socketPath, + database: config.database + })); + }); +} +// [END getConnection] + +// [START main] +getConnection(function (err, connection) { + console.log(err, !!connection); + if (err) { + return console.error(err); + } + createTable(connection, function (err, result) { + console.log(err, !!result); + if (err) { + return console.error(err); + } + console.log(result); + connection.end(); + }); +}); +// [END main] +// [END createTables] diff --git a/appengine/cloudsql/create_tables.js b/appengine/cloudsql/create_tables.js deleted file mode 100644 index ec5d33750d..0000000000 --- a/appengine/cloudsql/create_tables.js +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2015-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. - -// [START all] -'use strict'; - -var mysql = require('mysql'); -var prompt = require('prompt'); - -prompt.start(); - -prompt.get(['host', 'user', 'password', 'database'], function (err, config) { - if (err) { return; } - - config.multipleStatements = true; - - var connection = mysql.createConnection(config); - - connection.query( - 'CREATE TABLE `visits` (' + - ' `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,' + - ' `timestamp` DATETIME NULL,' + - ' `userIp` VARCHAR(46) NULL,' + - ' PRIMARY KEY (`id`));', - function (err) { - if (err) { - throw err; - } - console.log('Done!'); - connection.end(); - } - ); -}); -// [END all] diff --git a/appengine/cloudsql/package.json b/appengine/cloudsql/package.json index e4593b6ce0..b9ba08a035 100644 --- a/appengine/cloudsql/package.json +++ b/appengine/cloudsql/package.json @@ -8,14 +8,9 @@ "engines": { "node": "~4.2" }, - "scripts": { - "start": "node app.js", - "monitor": "nodemon app.js", - "deploy": "gcloud app deploy" - }, "dependencies": { - "express": "^4.13.4", - "mysql": "^2.10.2", - "prompt": "^0.2.14" + "express": "^4.14.0", + "mysql": "^2.11.1", + "prompt": "^1.0.0" } } diff --git a/appengine/cloudsql/server.js b/appengine/cloudsql/server.js new file mode 100644 index 0000000000..9d8b01ef89 --- /dev/null +++ b/appengine/cloudsql/server.js @@ -0,0 +1,113 @@ +// Copyright 2015-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. + +// [START app] +'use strict'; + +// [START setup] +var express = require('express'); +var mysql = require('mysql'); +var crypto = require('crypto'); + +var app = express(); +app.enable('trust proxy'); +// [END setup] + +// [START connect] +// Connect to the database +var connection = mysql.createConnection({ + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + socketPath: process.env.MYSQL_SOCKET_PATH, + database: process.env.MYSQL_DATABASE +}); +// [END connect] + +// [START insertVisit] +/** + * Insert a visit record into the database. + * + * @param {object} visit The visit record to insert. + * @param {function} callback The callback function. + */ +function insertVisit (visit, callback) { + connection.query('INSERT INTO `visits` SET ?', visit, function (err) { + if (err) { + return callback(err); + } + return callback(); + }); +} +// [END insertVisit] + +// [START getVisits] +var SQL_STRING = 'SELECT `timestamp`, `userIp`\n' + + 'FROM `visits`\n' + + 'ORDER BY `timestamp` DESC\n' + + 'LIMIT 10;'; + +/** + * Retrieve the latest 10 visit records from the database. + * + * @param {function} callback The callback function. + */ +function getVisits (callback) { + connection.query(SQL_STRING, function (err, results) { + if (err) { + return callback(err); + } + + return callback(null, results.map(function (visit) { + return 'Time: ' + visit.timestamp + ', AddrHash: ' + visit.userIp; + })); + }); +} +// [END getVisits] + +app.get('/', function (req, res, next) { + // Create a visit record to be stored in the database + var visit = { + timestamp: new Date(), + // Store a hash of the visitor's ip address + userIp: crypto.createHash('sha256').update(req.ip).digest('hex').substr(0, 7) + }; + + insertVisit(visit, function (err) { + if (err) { + return next(err); + } + + // Query the last 10 visits from the database. + getVisits(function (err, visits) { + if (err) { + return next(err); + } + + return res + .status(200) + .set('Content-Type', 'text/plain') + .send('Last 10 visits:\n' + visits.join('\n')); + }); + }); +}); + +// [START listen] +var server = app.listen(process.env.PORT || 8080, function () { + console.log('App listening on port %s', server.address().port); + console.log('Press Ctrl+C to quit.'); +}); +// [END listen] +// [END app] + +module.exports = app; diff --git a/test/appengine/all.test.js b/test/appengine/all.test.js index bde97c8834..eddb5f29bf 100644 --- a/test/appengine/all.test.js +++ b/test/appengine/all.test.js @@ -54,9 +54,9 @@ var sampleTests = [ { dir: 'appengine/cloudsql', cmd: 'node', - args: ['app.js'], + args: ['server.js'], msg: 'Last 10 visits:', - TRAVIS: true + TRAVIS_NODE_VERSION: '4' }, { dir: 'appengine/datastore', diff --git a/test/appengine/cloudsql/createTables.test.js b/test/appengine/cloudsql/createTables.test.js new file mode 100644 index 0000000000..d71d9d8c5f --- /dev/null +++ b/test/appengine/cloudsql/createTables.test.js @@ -0,0 +1,114 @@ +// 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 path = require('path'); +var proxyquire = require('proxyquire').noPreserveCache(); + +var SAMPLE_PATH = path.join(__dirname, '../../../appengine/cloudsql/createTables.js'); + +function getSample () { + var connectionMock = { + query: sinon.stub(), + end: sinon.stub() + }; + connectionMock.query.onFirstCall().callsArgWith(1, null, 'created visits table!'); + var mysqlMock = { + createConnection: sinon.stub().returns(connectionMock) + }; + var configMock = { + user: 'user', + password: 'password', + database: 'database', + socketPath: 'socketPath' + }; + var promptMock = { + start: sinon.stub(), + get: sinon.stub().callsArgWith(1, null, configMock) + }; + + proxyquire(SAMPLE_PATH, { + mysql: mysqlMock, + prompt: promptMock + }); + return { + mocks: { + connection: connectionMock, + mysql: mysqlMock, + config: configMock, + prompt: promptMock + } + }; +} + +describe('appengine/cloudsql/createTables.js', function () { + it('should record a visit', function (done) { + var sample = getSample(); + var expectedResult = 'created visits table!'; + + assert(sample.mocks.prompt.start.calledOnce); + assert(sample.mocks.prompt.get.calledOnce); + assert.deepEqual(sample.mocks.prompt.get.firstCall.args[0], [ + 'socketPath', + 'user', + 'password', + 'database' + ]); + + setTimeout(function () { + assert.deepEqual(sample.mocks.mysql.createConnection.firstCall.args[0], sample.mocks.config); + assert(console.log.calledWith(expectedResult)); + done(); + }, 10); + }); + + it('should handle prompt error', function (done) { + var expectedResult = 'createTables_prompt_error'; + var sample = getSample(); + + proxyquire(SAMPLE_PATH, { + mysql: sample.mocks.mysql, + prompt: { + start: sinon.stub(), + get: sinon.stub().callsArgWith(1, expectedResult) + } + }); + + setTimeout(function () { + assert(console.error.calledWith(expectedResult)); + done(); + }, 10); + }); + + it('should handle insert error', function (done) { + var expectedResult = 'createTables_insert_error'; + var sample = getSample(); + + var connectionMock = { + query: sinon.stub().callsArgWith(1, expectedResult) + }; + + proxyquire(SAMPLE_PATH, { + mysql: { + createConnection: sinon.stub().returns(connectionMock) + }, + prompt: sample.mocks.prompt + }); + + setTimeout(function () { + assert(console.error.calledWith(expectedResult)); + done(); + }, 10); + }); +}); diff --git a/test/appengine/cloudsql/server.test.js b/test/appengine/cloudsql/server.test.js new file mode 100644 index 0000000000..e256b8a143 --- /dev/null +++ b/test/appengine/cloudsql/server.test.js @@ -0,0 +1,127 @@ +// 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 express = require('express'); +var path = require('path'); +var proxyquire = require('proxyquire').noPreserveCache(); +var request = require('supertest'); + +var SAMPLE_PATH = path.join(__dirname, '../../../appengine/cloudsql/server.js'); + +function getSample () { + var serverMock = { + address: sinon.stub().returns({ + port: 8080 + }) + }; + var testApp = express(); + sinon.stub(testApp, 'listen', function (port, callback) { + assert.equal(port, 8080); + setTimeout(function () { + callback(); + }); + return serverMock; + }); + var expressMock = sinon.stub().returns(testApp); + var resultsMock = [ + { + timestamp: '1234', + userIp: 'abcd' + } + ]; + var connectionMock = { + query: sinon.stub() + }; + connectionMock.query.onFirstCall().callsArg(2); + connectionMock.query.onSecondCall().callsArgWith(1, null, resultsMock); + + var mysqlMock = { + createConnection: sinon.stub().returns(connectionMock) + }; + + var app = proxyquire(SAMPLE_PATH, { + mysql: mysqlMock, + express: expressMock + }); + return { + app: app, + mocks: { + server: serverMock, + express: expressMock, + results: resultsMock, + connection: connectionMock, + mysql: mysqlMock + } + }; +} + +describe('appengine/cloudsql/server.js', function () { + var sample; + + beforeEach(function () { + sample = getSample(); + + assert(sample.mocks.express.calledOnce); + assert(sample.mocks.mysql.createConnection.calledOnce); + assert.deepEqual(sample.mocks.mysql.createConnection.firstCall.args[0], { + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + socketPath: process.env.MYSQL_SOCKET_PATH, + database: process.env.MYSQL_DATABASE + }); + assert(sample.app.listen.calledOnce); + assert.equal(sample.app.listen.firstCall.args[0], process.env.PORT || 8080); + }); + + it('should record a visit', function (done) { + var expectedResult = 'Last 10 visits:\nTime: 1234, AddrHash: abcd'; + + request(sample.app) + .get('/') + .expect(200) + .expect(function (response) { + assert.equal(response.text, expectedResult); + }) + .end(done); + }); + + it('should handle insert error', function (done) { + var expectedResult = 'insert_error'; + + sample.mocks.connection.query.onFirstCall().callsArgWith(2, expectedResult); + + request(sample.app) + .get('/') + .expect(500) + .expect(function (response) { + assert.equal(response.text, expectedResult + '\n'); + }) + .end(done); + }); + + it('should handle read error', function (done) { + var expectedResult = 'read_error'; + + sample.mocks.connection.query.onSecondCall().callsArgWith(1, expectedResult); + + request(sample.app) + .get('/') + .expect(500) + .expect(function (response) { + assert.equal(response.text, expectedResult + '\n'); + }) + .end(done); + }); +});