diff --git a/.gitignore b/.gitignore index 4198ff08..7f329195 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /.vscode -/node_modules +node_modules /coverage firebase-debug.log npm-debug.log diff --git a/gulpfile.js b/gulpfile.js index 989ac053..4d497745 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -25,6 +25,10 @@ var paths = { tests: [ 'test/**/*.spec.js' + ], + + scripts: [ + 'scripts/*.js' ] }; @@ -34,7 +38,7 @@ var paths = { /***********/ // Lints the JavaScript files gulp.task('lint', function() { - var filesToLint = _.union(paths.js, paths.tests); + var filesToLint = _.union(paths.js, paths.tests, paths.scripts); return gulp.src(filesToLint) .pipe(eslint()) .pipe(eslint.format()) diff --git a/scripts/assets/functions_to_test.js b/scripts/assets/functions_to_test.js new file mode 100644 index 00000000..579963d5 --- /dev/null +++ b/scripts/assets/functions_to_test.js @@ -0,0 +1,28 @@ +var functions = require('firebase-functions'); + +exports.dbAction = functions.database().path('/input/{uuid}').onWrite(function(event) { + return event.data.ref.root.child('output/' + event.params.uuid).set(event.data.val()); +}); + +exports.nested = { + dbAction: functions.database().path('/inputNested/{uuid}').onWrite(function(event) { + return event.data.ref.root.child('output/' + event.params.uuid).set(event.data.val()); + }) +}; + +exports.httpsAction = functions.cloud.https().onRequest(function(req, res) { + res.send(req.body); +}); + +exports.pubsubAction = functions.cloud.pubsub('topic1').onPublish(function(event) { + var uuid = event.data.json; + var app = functions.app; + return app.database().ref('output/' + uuid).set(uuid); +}); + +exports.gcsAction = functions.cloud.storage('functions-integration-test.appspot.com') + .onChange(function(event) { + var uuid = event.data.data.name; + var app = functions.app; + return app.database().ref('output/' + uuid).set(uuid); + }); \ No newline at end of file diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 00000000..6a66a858 --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,14 @@ +{ + "name": "firebase-functions-integration-test", + "version": "1.0.0", + "description": "Integration test for deploying Firebase functions", + "main": "test-functions-deploy.js", + "scripts": { + "test": "node test-functions-deploy.js" + }, + "author": "Firebase", + "license": "MIT", + "dependencies": { + "firebase": "^3.5.0" + } +} diff --git a/scripts/test-functions-deploy.js b/scripts/test-functions-deploy.js new file mode 100644 index 00000000..ac32accd --- /dev/null +++ b/scripts/test-functions-deploy.js @@ -0,0 +1,203 @@ +#!/usr/bin/env node +'use strict'; + +var expect = require('chai').expect; +var execSync = require('child_process').execSync; +var exec = require('child_process').exec; +var tmp = require('tmp'); +var _ = require('lodash'); +var fs = require('fs-extra'); +var cloudfunctions = require('../lib/gcp/cloudfunctions'); +var api = require('../lib/api'); +var scopes = require('../lib/scopes'); +var configstore = require('../lib/configstore'); +var extractTriggers = require('../lib/extractTriggers'); +var RSVP = require('rsvp'); +var chalk = require('chalk'); +var firebase = require('firebase'); + +var functionsSource = __dirname + '/assets/functions_to_test.js'; +var projectDir = __dirname + '/test-project'; +var projectId = 'functions-integration-test'; +var httpsTrigger = 'https://us-central1-functions-integration-test.cloudfunctions.net/httpsAction'; +var region = 'us-central1'; +var localFirebase = __dirname + '/../bin/firebase'; +var TIMEOUT = 40000; +var tmpDir; +var app; + +var parseFunctionsList = function() { + var triggers = []; + extractTriggers(require(tmpDir + '/functions'), triggers); + return _.map(triggers, 'name'); +}; + +var getUuid = function() { + return Math.floor(Math.random() * 100000000000).toString(); +}; + +var preTest = function() { + var dir = tmp.dirSync({prefix: 'fntest_'}); + tmpDir = dir.name; + fs.copySync(projectDir, tmpDir); + execSync('npm install', {'cwd': tmpDir + '/functions'}); + api.setToken(configstore.get('tokens').refresh_token); + api.setScopes(scopes.CLOUD_PLATFORM); + var config = { + apiKey: 'AIzaSyCLgng7Qgzf-2UKRPLz--LtLLxUsMK8oco', + authDomain: 'functions-integration-test.firebaseapp.com', + databaseURL: 'https://functions-integration-test.firebaseio.com', + storageBucket: 'functions-integration-test.appspot.com' + }; + app = firebase.initializeApp(config); + console.log('Done pretest prep.'); +}; + +var postTest = function() { + fs.remove(tmpDir); + execSync(localFirebase + ' database:remove / -y', {'cwd': tmpDir}); + console.log('Done post-test cleanup.'); + process.exit(); +}; + +var checkFunctionsListMatch = function(expectedFunctions) { + return cloudfunctions.list(projectId, region).then(function(result) { + var deployedFunctions = _.map(result, 'functionName'); + expect(_.isEmpty(_.xor(expectedFunctions, deployedFunctions))).to.be.true; + return true; + }).catch(function(err) { + expect(err).to.be.null; + }); +}; + +var testCreateUpdate = function() { + fs.copySync(functionsSource, tmpDir + '/functions/index.js'); + return new RSVP.Promise(function(resolve) { + exec(localFirebase + ' deploy', {'cwd': tmpDir}, function(err, stdout) { + console.log(stdout); + expect(err).to.be.null; + resolve(checkFunctionsListMatch(parseFunctionsList())); + }); + }); +}; + +var testDelete = function() { + return new RSVP.Promise(function(resolve) { + exec('> functions/index.js &&' + localFirebase + ' deploy', {'cwd': tmpDir}, function(err, stdout) { + console.log(stdout); + expect(err).to.be.null; + resolve(checkFunctionsListMatch([])); + }); + }); +}; + +var waitForAck = function(uuid, testDescription) { + return Promise.race([ + new Promise(function(resolve) { + var ref = firebase.database().ref('output').child(uuid); + var listener = ref.on('value', function(snap) { + if (snap.exists()) { + ref.off('value', listener); + resolve(); + } + }); + }), + new Promise(function(resolve, reject) { + setTimeout(function() { + reject('Timed out while waiting for output from ' + testDescription); + }, TIMEOUT); + }) + ]); +}; + +var writeToDB = function(path) { + var uuid = getUuid(); + return app.database().ref(path).child(uuid).set({'foo': 'bar'}).then(function() { + return RSVP.resolve(uuid); + }); +}; + +var sendHttpRequest = function(message) { + return api.request('POST', httpsTrigger, { + data: message, + origin: '' + }).then(function(resp) { + expect(resp.status).to.equal(200); + expect(resp.body).to.deep.equal(message); + }); +}; + +var publishPubsub = function(topic) { + var uuid = getUuid(); + var message = new Buffer(uuid).toString('base64'); + return api.request('POST', '/v1/projects/functions-integration-test/topics/' + topic + ':publish', { + auth: true, + data: {'messages': [ + {'data': message} + ]}, + origin: 'https://pubsub.googleapis.com' + }).then(function(resp) { + expect(resp.status).to.equal(200); + return RSVP.resolve(uuid); + }); +}; + +var saveToStorage = function() { + var uuid = getUuid(); + var contentLength = Buffer.byteLength(uuid, 'utf8'); + var resource = ['b', projectId + '.appspot.com', 'o'].join('/'); + var endpoint = '/upload/storage/v1/' + resource + '?uploadType=media&name=' + uuid; + return api.request('POST', endpoint, { + auth: true, + headers: { + 'Content-Type': 'text/plain', + 'Content-Length': contentLength + }, + data: uuid, + json: false, + origin: api.googleOrigin + }).then(function(resp) { + expect(resp.status).to.equal(200); + return RSVP.resolve(uuid); + }); +}; + +var testFunctionsTrigger = function() { + var checkDbAction = writeToDB('input').then(function(uuid) { + return waitForAck(uuid, 'database triggered function'); + }); + var checkNestedDbAction = writeToDB('inputNested').then(function(uuid) { + return waitForAck(uuid, 'nested database triggered function'); + }); + var checkHttpsAction = sendHttpRequest({'message': 'hello'}); + var checkPubsubAction = publishPubsub('topic1').then(function(uuid) { + return waitForAck(uuid, 'pubsub triggered function'); + }); + var checkGcsAction = saveToStorage().then(function(uuid) { + return waitForAck(uuid, 'storage triggered function'); + }); + return RSVP.all([checkDbAction, checkNestedDbAction, checkHttpsAction, checkPubsubAction, checkGcsAction]); +}; + +var main = function() { + preTest(); + testCreateUpdate().then(function() { + console.log(chalk.green('\u2713 Test passed: creating functions')); + return testCreateUpdate(); + }).then(function() { + console.log(chalk.green('\u2713 Test passed: updating functions')); + return testFunctionsTrigger(); + }).then(function() { + console.log(chalk.green('\u2713 Test passed: triggering functions')); + return testDelete(); + }).then(function() { + console.log(chalk.green('\u2713 Test passed: deleting functions')); + }).catch(function(err) { + console.log(chalk.red('Error while running tests: '), err); + return RSVP.resolve(); + }).then(postTest); +}; + +main(); + + diff --git a/scripts/test-project/.firebaserc b/scripts/test-project/.firebaserc new file mode 100644 index 00000000..23612cfb --- /dev/null +++ b/scripts/test-project/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "functions-integration-test" + } +} diff --git a/scripts/test-project/database.rules.json b/scripts/test-project/database.rules.json new file mode 100644 index 00000000..b104e9c2 --- /dev/null +++ b/scripts/test-project/database.rules.json @@ -0,0 +1,6 @@ +{ + "rules": { + ".read": true, + ".write": true + } +} \ No newline at end of file diff --git a/scripts/test-project/firebase.json b/scripts/test-project/firebase.json new file mode 100644 index 00000000..e7c9fc27 --- /dev/null +++ b/scripts/test-project/firebase.json @@ -0,0 +1,8 @@ +{ + "database": { + "rules": "database.rules.json" + }, + "hosting": { + "public": "public" + } +} diff --git a/scripts/test-project/functions/package.json b/scripts/test-project/functions/package.json new file mode 100644 index 00000000..0a25ea63 --- /dev/null +++ b/scripts/test-project/functions/package.json @@ -0,0 +1,8 @@ +{ + "name": "functions", + "description": "Firebase Functions", + "dependencies": { + "firebase": "^3.1", + "firebase-functions": "https://storage.googleapis.com/firebase-preview-drop/node/firebase-functions/firebase-functions-preview.latest.tar.gz" + } +} diff --git a/scripts/test-project/public/404.html b/scripts/test-project/public/404.html new file mode 100644 index 00000000..e7a436b5 --- /dev/null +++ b/scripts/test-project/public/404.html @@ -0,0 +1,4 @@ + + + + diff --git a/scripts/test-project/public/index.html b/scripts/test-project/public/index.html new file mode 100644 index 00000000..a37c7330 --- /dev/null +++ b/scripts/test-project/public/index.html @@ -0,0 +1,7 @@ + + + + + + +