diff --git a/.gitignore b/.gitignore index cfe66ee..2395f73 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ lib .npmrc .vscode *.tgz -.tmp \ No newline at end of file +.tmp +*.log \ No newline at end of file diff --git a/package.json b/package.json index cf466e1..fd400b1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "pretest": "node_modules/.bin/tsc", "test": "mocha .tmp/spec/index.spec.js", "posttest": "npm run lint && rm -rf .tmp", + "preintegrationTest": "node_modules/.bin/tsc", + "integrationTest": "firebase emulators:exec --project=not-a-project --only firestore 'mocha .tmp/spec/integration/**/*.spec.js'", + "postintegrationTest": "rm -rf .tmp", "format": "prettier --check '**/*.{json,ts,yml,yaml}'", "format:fix": "prettier --write '**/*.{json,ts,yml,yaml}'" }, @@ -44,6 +47,7 @@ "chai": "^4.2.0", "firebase-admin": "~8.9.0", "firebase-functions": "^3.3.0", + "firebase-tools": "^8.9.2", "mocha": "^6.2.2", "prettier": "^1.19.1", "sinon": "^7.5.0", diff --git a/spec/integration/providers/firestore.spec.ts b/spec/integration/providers/firestore.spec.ts new file mode 100644 index 0000000..406e84f --- /dev/null +++ b/spec/integration/providers/firestore.spec.ts @@ -0,0 +1,44 @@ +import { expect } from 'chai'; +import { firestore, initializeApp } from 'firebase-admin'; +import fft = require('../../../src/index'); + +describe('providers/firestore', () => { + before(() => { + initializeApp(); + }); + + it('clears database with clearFirestoreData', async () => { + const test = fft({ projectId: 'not-a-project' }); + const db = firestore(); + + await Promise.all([ + db + .collection('test') + .doc('doc1') + .set({}), + db + .collection('test') + .doc('doc1') + .collection('test') + .doc('doc2') + .set({}), + ]); + + await test.firestore.clearFirestoreData({ projectId: 'not-a-project' }); + + const docs = await Promise.all([ + db + .collection('test') + .doc('doc1') + .get(), + db + .collection('test') + .doc('doc1') + .collection('test') + .doc('doc2') + .get(), + ]); + expect(docs[0].exists).to.be.false; + expect(docs[1].exists).to.be.false; + }); +}); diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index 42944c4..b584461 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -27,6 +27,8 @@ import { has, get, isEmpty, isPlainObject, mapValues } from 'lodash'; import { testApp } from '../app'; +import * as http from 'http'; + /** Optional parameters for creating a DocumentSnapshot. */ export interface DocumentSnapshotOptions { /** ISO timestamp string for the snapshot was read, default is current time. */ @@ -214,3 +216,58 @@ export function objectToValueProto(data: object) { return mapValues(data, encodeHelper); } + +const FIRESTORE_ADDRESS_ENVS = [ + 'FIRESTORE_EMULATOR_HOST', + 'FIREBASE_FIRESTORE_EMULATOR_ADDRESS', +]; + +const FIRESTORE_ADDRESS = FIRESTORE_ADDRESS_ENVS.reduce( + (addr, name) => process.env[name] || addr, + 'localhost:8080' +); +const FIRESTORE_PORT = FIRESTORE_ADDRESS.split(':')[1]; + +/** Clears all data in firestore. Works only in offline mode. + */ +export function clearFirestoreData(options: { projectId: string } | string) { + return new Promise((resolve, reject) => { + let projectId; + + if (typeof options === 'string') { + projectId = options; + } else if (typeof options === 'object' && has(options, 'projectId')) { + projectId = options.projectId; + } else { + throw new Error('projectId not specified'); + } + + const config = { + method: 'DELETE', + hostname: 'localhost', + port: FIRESTORE_PORT, + path: `/emulator/v1/projects/${projectId}/databases/(default)/documents`, + }; + + const req = http.request(config, (res) => { + if (res.statusCode !== 200) { + reject(new Error(`statusCode: ${res.statusCode}`)); + } + res.on('data', () => {}); + res.on('end', resolve); + }); + + req.on('error', (error) => { + reject(error); + }); + + const postData = JSON.stringify({ + database: `projects/${projectId}/databases/(default)`, + }); + + req.setHeader('Content-Length', postData.length); + + req.write(postData); + req.end(); + }); +}