diff --git a/.gitignore b/.gitignore index d49592be1d9..39aea992530 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ **/node_modules/** npm-debug.log -coverage/ \ No newline at end of file +coverage/ + +test/encrypted/secrets.tar +test/encrypted/express-demo.json +test/encrypted/hapi-demo.json \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index dce4356f0c0..c12b59f0ee5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -sudo: false +sudo: required language: node_js node_js: - "stable" @@ -20,6 +20,7 @@ cache: services: - redis-server + - docker env: - PATH=$PATH:$HOME/gcloud/google-cloud-sdk/bin GOOGLE_APPLICATION_CREDENTIALS=$TRAVIS_BUILD_DIR/nodejs-docs-samples.json TEST_BUCKET_NAME=cloud-samples-tests TEST_PROJECT_ID=cloud-samples-tests #Other environment variables on same line @@ -38,12 +39,25 @@ before_install: - if [ ! -d $HOME/gcloud/google-cloud-sdk ]; then mkdir -p $HOME/gcloud && - wget https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz --directory-prefix=$HOME/gcloud && + wget https://dl.google.com/dl/cloudsdk/channels/rapid/google-cloud-sdk.tar.gz --directory-prefix=$HOME/gcloud && cd $HOME/gcloud && tar xzf google-cloud-sdk.tar.gz && printf '\ny\n\ny\ny\n' | ./google-cloud-sdk/install.sh && + source /home/travis/.bash_profile && cd $TRAVIS_BUILD_DIR; fi +- gcloud components update -q +- gcloud components update app -q +- openssl aes-256-cbc -K $encrypted_4e84c7c7ab67_key -iv $encrypted_4e84c7c7ab67_iv -in test/encrypted/secrets.tar.enc -out test/encrypted/secrets.tar -d +- if [ -a test/encrypted/secrets.tar ]; then + cd test/encrypted && tar xvf secrets.tar && cd ../..; + fi +- if [ -a test/encrypted/express-demo.json ]; then + gcloud auth activate-service-account --key-file test/encrypted/express-demo.json; + fi +- if [ -a test/encrypted/hapi-demo.json ]; then + gcloud auth activate-service-account --key-file test/encrypted/hapi-demo.json; + fi - openssl aes-256-cbc -K $encrypted_95e832a36b06_key -iv $encrypted_95e832a36b06_iv -in nodejs-docs-samples.json.enc -out nodejs-docs-samples.json -d - if [ -a nodejs-docs-samples.json ]; then gcloud auth activate-service-account --key-file nodejs-docs-samples.json; diff --git a/package.json b/package.json index ddf938af216..1ff43530246 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "googleapis": "~2.1.3" }, "devDependencies": { + "async": "^1.5.0", "coveralls": "^2.11.4", "istanbul": "^0.4.0", "jshint": "~2.8.0", diff --git a/test/appengine/test.js b/test/appengine/test.js index 74c3030e18d..2d5e0961e9e 100644 --- a/test/appengine/test.js +++ b/test/appengine/test.js @@ -15,6 +15,7 @@ var spawn = require('child_process').spawn; var request = require('request'); +var async = require('async'); var cwd = process.cwd(); @@ -25,6 +26,8 @@ function getPath(dir) { var sampleTests = [ { dir: 'express', + projectId: 'express-demo', + account: 'account-2@express-demo.iam.gserviceaccount.com', cmd: 'node', arg1: './bin/www', msg: 'Hello World! Express.js on Google App Engine.' @@ -43,6 +46,8 @@ var sampleTests = [ }, { dir: 'hapi', + projectId: 'hapi-demo', + 'account': 'account-1@hapi-demo.iam.gserviceaccount.com', cmd: 'node', arg1: 'index.js', msg: 'Hello World! Hapi.js on Google App Engine.' @@ -93,97 +98,264 @@ if (process.env.TRAVIS_NODE_VERSION !== 'stable') { }); } +// Send a request to the given url and test that the response body has the +// expected value +function testRequest(url, sample, cb) { + request(url, function (err, res, body) { + if (err) { + // Request error + return cb(err); + } else { + if (body && body.indexOf(sample.msg) !== -1 && + (res.statusCode === 200 || res.statusCode === sample.code)) { + // Success + return cb(null, true); + } else { + // Short-circuit app test + var message = sample.dir + ': failed verification!\n' + + 'Expected: ' + sample.msg + '\n' + + 'Actual: ' + body; + + // Response body did not match expected + return cb(new Error(message)); + } + } + }); +} + +// Helper for deciding whether to hide certain noisy console output +function shouldHide(data) { + if (data && typeof data === 'function' && (data.indexOf('.../') !== -1 || + data.indexOf('...-') !== -1 || + data.indexOf('...\\') !== -1 || + data.indexOf('...|') !== -1)) { + return true; + } +} + describe('appengine/', function () { sampleTests.forEach(function (sample) { it(sample.dir + ': dependencies should install', function (done) { + // Allow extra time for "npm install" this.timeout(sample.timeout || 120000); - var calledDone = false; - var proc = spawn('npm', ['install'], { - cwd: getPath(sample.dir) - }); + testInstallation(sample, done); + }); - proc.on('error', function (err) { - if (!calledDone) { - calledDone = true; - done(err); - } - }); + it(sample.dir + ': should return 200 and Hello World', function (done) { + testLocalApp(sample, done); + }); + }); - if (!process.env.TRAVIS) { - proc.stderr.on('data', function (data) { - console.log('stderr: ' + data); - }); - } + if (!process.env.TRAVIS || process.env.TRAVIS_NODE_VERSION !== 'stable') { + return; + } - proc.on('exit', function (code) { - if (!calledDone) { - calledDone = true; - if (code !== 0) { - done(new Error(sample.dir + ': failed to install dependencies!')); - } else { - done(); - } - } - }); + it('should deploy all samples', function (done) { + // 10 minutes because deployments are slow + this.timeout(10 * 60 * 1000); + + testDeployments(done); + }); +}); + +function testInstallation(sample, done) { + + // Keep track off whether "done" has been called yet + var calledDone = false; + + var proc = spawn('npm', ['install'], { + cwd: getPath(sample.dir) + }); + + proc.on('error', finish); + + if (!process.env.TRAVIS) { + proc.stderr.on('data', function (data) { + console.log('stderr: ' + data); }); + } - it(sample.dir + ': should return 200 and Hello World', function (done) { - var timeoutId; - var intervalId; - var success = false; + proc.on('exit', function (code) { + if (code !== 0) { + finish(new Error(sample.dir + ': failed to install dependencies!')); + } else { + finish(); + } + }); + + // Exit helper so we don't call "cb" more than once + function finish(err) { + if (!calledDone) { + calledDone = true; + done(err); + } + } +} + +function testLocalApp(sample, done) { + var calledDone = false; + + var proc = spawn(sample.cmd, [sample.arg1], { + cwd: getPath(sample.dir) + }); + + proc.on('error', finish); + + if (!process.env.TRAVIS) { + proc.stderr.on('data', function (data) { + console.log('stderr: ' + data); + }); + } + + proc.on('exit', function (code, signal) { + if (code !== 0 && signal !== 'SIGKILL') { + return finish(new Error(sample.dir + ': failed to run!')); + } else { + return finish(); + } + }); + + // Give the server time to start up + setTimeout(function () { + // Test that the app is working + testRequest('http://localhost:8080', sample, function (err) { + proc.kill('SIGKILL'); + return finish(err); + }); + }, 5000); + + // Exit helper so we don't call "cb" more than once + function finish(err) { + if (!calledDone) { + calledDone = true; + done(err); + } + } +} + +function testDeployments(done) { + + // Only deploy samples that have a projectId + var samplesToDeploy = sampleTests.filter(function (sample) { + return sample.projectId; + }); + + // Create deployment tasks + var tasks = samplesToDeploy.map(function (sample) { + return function (cb) { + // Keep track off whether "cb" has been called yet var calledDone = false; - var proc = spawn(sample.cmd, [sample.arg1], { - cwd: getPath(sample.dir) + var _cwd = getPath(sample.dir); + var args = [ + 'preview', + 'app', + 'deploy', + 'app.yaml', + // Skip prompt + '-q', + '--project', + sample.projectId, + '--promote', + // Deploy over existing version so we don't have to clean up + '--version', + 'demo', + // Use the service account for the sample's gcloud project + '--account', + sample.account, + // Override any existing deployment + '--force', + // Build locally, much faster + '--docker-build', + 'local' + ]; + + console.log(_cwd + ' $ gcloud ' + args.join(' ')); + + // Don't use "npm run deploy" because we need extra flags + var proc = spawn('gcloud', args, { + cwd: _cwd }); - proc.on('error', function (err) { + // Exit helper so we don't call "cb" more than once + function finish(err, result) { if (!calledDone) { calledDone = true; - done(err); + cb(err, result); } - }); - - if (!process.env.TRAVIS) { - proc.stderr.on('data', function (data) { - console.log('stderr: ' + data); - }); } - proc.on('exit', function (code, signal) { - if (!calledDone) { - calledDone = true; - if (code !== 0 && signal !== 'SIGKILL') { - done(new Error(sample.dir + ': failed to run!')); - } else { - if (!success) { - done(new Error(sample.dir + ': failed verification!')); - } else { - done(); - } - } + // Print stderr of process + proc.stderr.on('data', function (data) { + if (!shouldHide(data)) { + console.log(sample.projectId + ' stderr: ' + data); } }); - timeoutId = setTimeout(end, 5000); - intervalId = setInterval(testRequest, 1000); + // Print stdout of process + proc.stdout.on('data', function (data) { + if (!shouldHide(data)) { + console.log(sample.projectId + ' stdout: ' + data); + } + }); - function end() { - clearTimeout(timeoutId); - clearInterval(intervalId); - proc.kill('SIGKILL'); - } + // This is called if the process fails to start. "error" event may or may + // not be fired in addition to the "exit" event. + proc.on('error', finish); - function testRequest() { - request('http://localhost:8080', function (err, res, body) { - if (body && body.indexOf(sample.msg) !== -1 && - (res.statusCode === 200 || res.statusCode === sample.code)) { - success = true; - end(); - } - }); + // Process has completed + proc.on('exit', function (code) { + if (code !== 0) { // Deployment failed + // Pass error as second argument so we don't short-circuit the + // parallel tasks + return finish(null, new Error(sample.dir + ': failed to deploy!')); + } else { // Deployment succeeded + // Test that sample app is running successfully + return async.waterfall([ + function (cb) { + // Give apps time to start + setTimeout(cb, 5000); + }, + function (cb) { + // Test "default" module + var url = 'http://' + sample.projectId + '.appspot.com'; + testRequest(url, sample, cb); + }, + function (result, cb) { + // Test versioned url of "default" module + var demoUrl = 'http://demo.' + sample.projectId + '.appspot.com'; + testRequest(demoUrl, sample, cb); + } + ], finish); + } + }); + }; + }); + + // Deploy sample apps in parallel + return async.parallel(tasks, function (err, results) { + if (err) { + return done(err); + } else { + var success = true; + var message = ''; + // Find errors that didn't short-circuit the parallel tasks + results.forEach(function (result) { + if (result instanceof Error) { + // Gather error messages + message = message + result.message + '\n'; + success = false; + } else { + // "result" should be "true" for those apps that passed verification + success = success && result; + } + }); + if (success) { + return done(); + } else { + return done(new Error(message)); } - }); + } }); -}); +} diff --git a/test/encrypted/secrets.tar.enc b/test/encrypted/secrets.tar.enc new file mode 100644 index 00000000000..3a73d01e7b0 Binary files /dev/null and b/test/encrypted/secrets.tar.enc differ