diff --git a/.travis.yml b/.travis.yml index d4aaf919263..01046cdde54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ jobs: - script: yarn test:saucelabs --database-firestore-only # TODO(yifanyang): Once we verify the emulator tests are reliable, we # should make these tests blocking rather than allow failures. - - script: node scripts/testing/database-emulator-test.js + - script: yarn test:database:emulator include: - name: Node.js and Browser (Chrome) Test stage: test @@ -53,7 +53,7 @@ jobs: if: type = push - name: Database Node.js and Browser (Chrome) Test with Emulator stage: test - script: node scripts/testing/database-emulator-test.js + script: yarn test:database:emulator - stage: deploy script: skip # NPM Canary Build Config diff --git a/package.json b/package.json index 8547d216a01..d387c6b1988 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,9 @@ "test:coverage": "lcov-result-merger 'packages/**/lcov.info' | coveralls", "test:setup": "node tools/config.js", "pretest:saucelabs": "lerna run --parallel pretest", - "test:saucelabs": "karma start config/karma.saucelabs.js --single-run" + "test:saucelabs": "karma start config/karma.saucelabs.js --single-run", + "test:database:emulator": "ts-node scripts/emulator-testing/database-test-runner.ts", + "test:firestore:emulator": "ts-node scripts/emulator-testing/firestore-test-runner.ts" }, "repository": { "type": "git", @@ -61,6 +63,8 @@ "prettier": "1.12.0", "semver": "5.5.0", "simple-git": "1.92.0", + "ts-node": "5.0.1", + "typescript": "2.8.1", "yargs": "11.0.0" } } diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index 1b69a7bfc59..7d61e3ecdd5 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -27,22 +27,39 @@ declare const __karma__: any; const PROJECT_CONFIG = require('../../../../../config/project.json'); +const EMULATOR_PORT = process.env.FIRESTORE_EMULATOR_PORT; +const EMULATOR_PROJECT_ID = process.env.FIRESTORE_EMULATOR_PROJECT_ID; +export const USE_EMULATOR = !!EMULATOR_PORT; + +const EMULATOR_FIRESTORE_SETTING = { + host: `localhost:${EMULATOR_PORT}`, + ssl: false, + timestampsInSnapshots: true +}; + +const PROD_FIRESTORE_SETTING = { + host: 'firestore.googleapis.com', + ssl: true, + timestampsInSnapshots: true +}; + export const DEFAULT_SETTINGS = getDefaultSettings(); +// tslint:disable-next-line:no-console +console.log(`Default Settings: ${JSON.stringify(DEFAULT_SETTINGS)}`); + function getDefaultSettings(): firestore.Settings { const karma = typeof __karma__ !== 'undefined' ? __karma__ : undefined; if (karma && karma.config.firestoreSettings) { return karma.config.firestoreSettings; } else { - return { - host: 'firestore.googleapis.com', - ssl: true, - timestampsInSnapshots: true - }; + return USE_EMULATOR ? EMULATOR_FIRESTORE_SETTING : PROD_FIRESTORE_SETTING; } } -export const DEFAULT_PROJECT_ID = PROJECT_CONFIG.projectId; +export const DEFAULT_PROJECT_ID = USE_EMULATOR + ? EMULATOR_PROJECT_ID + : PROJECT_CONFIG.projectId; export const ALT_PROJECT_ID = 'test-db2'; function isIeOrEdge(): boolean { diff --git a/scripts/emulator-testing/database-test-runner.ts b/scripts/emulator-testing/database-test-runner.ts new file mode 100644 index 00000000000..500d6885553 --- /dev/null +++ b/scripts/emulator-testing/database-test-runner.ts @@ -0,0 +1,50 @@ +/** + * 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. + */ + +import { spawn } from 'child-process-promise'; +import * as path from 'path'; + +import { DatabaseEmulator } from './emulators/database-emulator'; +import { ChildProcessPromise } from './emulators/emulator'; + +function runTest(port: number, namespace: string): ChildProcessPromise { + const options = { + cwd: path.resolve(__dirname, '../../packages/database'), + env: Object.assign({}, process.env, { + RTDB_EMULATOR_PORT: port, + RTDB_EMULATOR_NAMESPACE: namespace + }), + stdio: 'inherit' + }; + return spawn('yarn', ['test'], options); +} + +async function run(): Promise { + const emulator = new DatabaseEmulator(); + try { + await emulator.download(); + await emulator.setUp(); + await emulator.setPublicRules(); + await runTest(emulator.port, emulator.namespace); + } finally { + await emulator.tearDown(); + } +} + +run().catch(err => { + console.error(err); + process.exitCode = 1; +}); diff --git a/scripts/emulator-testing/emulators/database-emulator.ts b/scripts/emulator-testing/emulators/database-emulator.ts new file mode 100644 index 00000000000..72c7198076d --- /dev/null +++ b/scripts/emulator-testing/emulators/database-emulator.ts @@ -0,0 +1,54 @@ +/** + * 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. + */ + +import * as request from 'request'; + +import { Emulator } from './emulator'; + +export class DatabaseEmulator extends Emulator { + namespace: string; + + constructor(port = 8088, namespace = 'test-emulator') { + super(port); + this.namespace = namespace; + this.binaryName = 'database-emulator.jar'; + // Use locked version of emulator for test to be deterministic. + // The latest version can be found from database emulator doc: + // https://firebase.google.com/docs/database/security/test-rules-emulator + this.binaryUrl = + 'https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v3.5.0.jar'; + } + + setPublicRules(): Promise { + console.log('Setting rule {".read": true, ".write": true} to emulator ...'); + return new Promise((resolve, reject) => { + request.put( + { + uri: `http://localhost:${this.port}/.settings/rules.json?ns=${ + this.namespace + }`, + headers: { Authorization: 'Bearer owner' }, + body: '{ "rules": { ".read": true, ".write": true } }' + }, + (error, response, body) => { + if (error) reject(error); + console.log(`Done setting public rule to emulator: ${body}.`); + resolve(response.statusCode); + } + ); + }); + } +} diff --git a/scripts/emulator-testing/emulators/emulator.ts b/scripts/emulator-testing/emulators/emulator.ts new file mode 100644 index 00000000000..7367d0d799b --- /dev/null +++ b/scripts/emulator-testing/emulators/emulator.ts @@ -0,0 +1,109 @@ +/** + * 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. + */ + +import { spawn } from 'child-process-promise'; +import { ChildProcess } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as request from 'request'; +import * as tmp from 'tmp'; + +export interface ChildProcessPromise extends Promise { + childProcess: ChildProcess; +} + +export abstract class Emulator { + binaryName: string; + binaryUrl: string; + binaryPath: string; + + emulator: ChildProcess; + port: number; + + constructor(port: number) { + this.port = port; + } + + download(): Promise { + return new Promise((resolve, reject) => { + tmp.dir((err, dir) => { + if (err) reject(err); + + console.log(`Created temporary directory at [${dir}].`); + const filepath: string = path.resolve(dir, this.binaryName); + const writeStream: fs.WriteStream = fs.createWriteStream(filepath); + + console.log(`Downloading emulator from [${this.binaryUrl}] ...`); + request(this.binaryUrl) + .pipe(writeStream) + .on('finish', () => { + console.log(`Saved emulator binary file to [${filepath}].`); + this.binaryPath = filepath; + resolve(); + }) + .on('error', reject); + }); + }); + } + + setUp(): Promise { + return new Promise((resolve, reject) => { + const promise: ChildProcessPromise = spawn( + 'java', + ['-jar', path.basename(this.binaryPath), '--port', this.port], + { + cwd: path.dirname(this.binaryPath), + stdio: 'inherit' + } + ); + promise.catch(reject); + this.emulator = promise.childProcess; + + console.log(`Waiting for emulator to start up ...`); + const timeout = 10; // seconds + const start: number = Date.now(); + + const wait = (resolve, reject) => { + if (Date.now() - start > timeout * 1000) { + reject(`Emulator not ready after ${timeout}s. Exiting ...`); + } else { + console.log(`Ping emulator at [http://localhost:${this.port}] ...`); + request(`http://localhost:${this.port}`, (error, response) => { + if (error && error.code === 'ECONNREFUSED') { + setTimeout(wait, 1000, resolve, reject); + } else if (response) { + // Database and Firestore emulators will return 400 and 200 respectively. + // As long as we get a response back, it means the emulator is ready. + console.log('Emulator has started up successfully!'); + resolve(); + } else { + // This should not happen. + reject({ error, response }); + } + }); + } + }; + setTimeout(wait, 1000, resolve, reject); + }); + } + + tearDown(): void { + if (this.emulator) { + console.log(`Shutting down emulator, pid: [${this.emulator.pid}] ...`); + this.emulator.kill(); + } + } +} diff --git a/scripts/emulator-testing/emulators/firestore-emulator.ts b/scripts/emulator-testing/emulators/firestore-emulator.ts new file mode 100644 index 00000000000..c7eb07736cd --- /dev/null +++ b/scripts/emulator-testing/emulators/firestore-emulator.ts @@ -0,0 +1,32 @@ +/** + * 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. + */ + +import { Emulator } from './emulator'; + +export class FirestoreEmulator extends Emulator { + projectId: string; + + constructor(port = 8087, projectId = 'test-emulator') { + super(port); + this.projectId = projectId; + this.binaryName = 'firestore-emulator.jar'; + // Use locked version of emulator for test to be deterministic. + // The latest version can be found from firestore emulator doc: + // https://firebase.google.com/docs/firestore/security/test-rules-emulator + this.binaryUrl = + 'https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-firestore-emulator-v1.2.1.jar'; + } +} diff --git a/scripts/emulator-testing/firestore-test-runner.ts b/scripts/emulator-testing/firestore-test-runner.ts new file mode 100644 index 00000000000..f8ad4948545 --- /dev/null +++ b/scripts/emulator-testing/firestore-test-runner.ts @@ -0,0 +1,51 @@ +/** + * 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. + */ + +import { spawn } from 'child-process-promise'; +import * as path from 'path'; + +import { ChildProcessPromise } from './emulators/emulator'; +import { FirestoreEmulator } from './emulators/firestore-emulator'; + +function runTest(port: number, projectId: string): ChildProcessPromise { + const options = { + cwd: path.resolve(__dirname, '../../packages/firestore'), + env: Object.assign({}, process.env, { + FIRESTORE_EMULATOR_PORT: port, + FIRESTORE_EMULATOR_PROJECT_ID: projectId + }), + stdio: 'inherit' + }; + // TODO(b/113267261): Include browser test once WebChannel support is + // ready in Firestore emulator. + return spawn('yarn', ['test:node'], options); +} + +async function run(): Promise { + const emulator = new FirestoreEmulator(); + try { + await emulator.download(); + await emulator.setUp(); + await runTest(emulator.port, emulator.projectId); + } finally { + await emulator.tearDown(); + } +} + +run().catch(err => { + console.error(err); + process.exitCode = 1; +}); diff --git a/scripts/emulator-testing/tsconfig.json b/scripts/emulator-testing/tsconfig.json new file mode 100644 index 00000000000..60604af5b7c --- /dev/null +++ b/scripts/emulator-testing/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../config/tsconfig.base.json" +} diff --git a/scripts/testing/database-emulator-test.js b/scripts/testing/database-emulator-test.js deleted file mode 100644 index a9956ec7413..00000000000 --- a/scripts/testing/database-emulator-test.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * 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. - */ - -const fs = require('fs'); -const path = require('path'); -const request = require('request'); -const tmp = require('tmp'); - -const { spawn } = require('child-process-promise'); - -const EMULATOR_LINK = - 'https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v3.5.0.jar'; -const EMULATOR_FILENAME = 'database-emulator.jar'; - -const EMULATOR_PORT = 8088; -const EMULATOR_NAMESPACE = 'test-emulator'; - -async function downloadEmulator() { - return new Promise((resolve, reject) => { - tmp.dir((err, dir) => { - if (err) reject(err); - console.log(`Created temporary directory at [${dir}].`); - const filepath = path.resolve(dir, EMULATOR_FILENAME); - const writeStream = fs.createWriteStream(filepath); - console.log(`Downloading emulator from [${EMULATOR_LINK}] ...`); - request(EMULATOR_LINK) - .pipe(writeStream) - .on('finish', () => { - console.log(`Saved emulator binary file to [${filepath}].`); - resolve(filepath); - }) - .on('error', reject); - }); - }); -} - -async function launchEmulator(filepath) { - return new Promise((resolve, reject) => { - const promise = spawn( - 'java', - ['-jar', path.basename(filepath), '--port', EMULATOR_PORT], - { - cwd: path.dirname(filepath), - stdio: 'inherit' - } - ); - promise.catch(reject); - - const timeout = 10; // seconds - console.log(`Waiting for emulator to start up ...`); - const start = Date.now(); - setTimeout(function wait() { - if (Date.now() - start > timeout * 1000) { - reject(`Emulator not ready after ${timeout}s. Exiting ...`); - } else { - console.log(`Ping emulator at [http://localhost:${EMULATOR_PORT}] ...`); - request(`http://localhost:${EMULATOR_PORT}`, (error, response) => { - if (error && error.code === 'ECONNREFUSED') { - setTimeout(wait, 1000); - } else if (response && response.statusCode === 400) { - // The emulator is ready to serve requests as long as it can return - // a response instead of a 'ECONNREFUSED' error. - // It is not necessary to send a valid requests here, as they - // are doing some certain things like setting data or rules to the - // emulator. - // More information on valid requests that emulator recognizes: - // https://firebase.google.com/docs/database/security/test-rules-emulator#interact_with_the_emulator - console.log('Emulator has started up successfully!'); - resolve(promise.childProcess); - } else { - // This should not happen. - reject({ error, response }); - } - }); - } - }, 1000); - }); -} - -async function setPublicRules() { - console.log('Setting rule {".read": true, ".write": true} to emulator ...'); - return new Promise((resolve, reject) => { - request.put( - { - uri: `http://localhost:${EMULATOR_PORT}/.settings/rules.json?ns=${EMULATOR_NAMESPACE}`, - headers: { Authorization: 'Bearer owner' }, - body: '{ "rules": { ".read": true, ".write": true } }' - }, - (error, response, body) => { - if (error) reject(error); - console.log(`Done setting public rule to emulator. Response: ${body}.`); - resolve(response.statusCode); - } - ); - }); -} - -async function runTest() { - const options = { - cwd: path.resolve(__dirname, '../../packages/database'), - env: Object.assign({}, process.env, { - RTDB_EMULATOR_PORT: EMULATOR_PORT, - RTDB_EMULATOR_NAMESPACE: EMULATOR_NAMESPACE - }), - stdio: 'inherit' - }; - return spawn('yarn', ['test'], options); -} - -async function start() { - let emulator; - try { - const filepath = await downloadEmulator(); - emulator = await launchEmulator(filepath); - await setPublicRules(); - await runTest(); - } finally { - if (emulator) { - console.log(`Shutting down emulator, pid: [${emulator.pid}] ...`); - emulator.kill(); - } - } -} - -start().catch(err => { - console.error(err); - process.exitCode = 1; -});