Skip to content

Commit f6712e0

Browse files
author
Robert-Jan Huijsman
authored
Add an extremely basic integration test to the SDK (#150)
Add an extremely basic integration test to the SDK, that SDK developers can run against their own Firebase projects to check that all moving parts still work in practice. Previously, many developers had something like this stashed somewhere on their machine. The hope is that by adding this to the Git repo we'll maintain the integration test in a single place as we add new SDK features.
1 parent adbaeab commit f6712e0

File tree

15 files changed

+381
-9
lines changed

15 files changed

+381
-9
lines changed

.gitignore

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
.idea
2-
.tmp
32
.npm
4-
npm-debug.log
5-
lib
6-
yarn.lock
3+
.tmp
4+
.vscode/
75
coverage
6+
firebase-functions-*.tgz
7+
integration_test/.firebaserc
8+
integration_test/*.log
9+
integration_test/functions/firebase-functions.tgz
10+
lib
811
node_modules
12+
npm-debug.log
913
typings
10-
.vscode/
11-
firebase-functions-*.tgz
14+
yarn.lock

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ tslint.*
99
# Don't include the raw typescript
1010
src
1111
spec
12+
integration_test
1213
# TODO(rjh) add back once testing isn't just a joke
1314
testing
1415
lib/testing.*

integration_test/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
How to Use
2+
---------
3+
4+
***ATTENTION***: Running this test will wipe the contents of the Firebase project you run it against. Make sure you use a disposable Firebase project!
5+
6+
Run the integration test as follows:
7+
8+
```bash
9+
firebase use $YOURPROJECTID # add your own project
10+
./run_tests.sh
11+
```
12+
13+
Follow the instructions output by the script. You'll click on a number of HTTPS function links. The integration test for HTTPS is that it properly kicks off other integration tests and redirects you to the database console. From there the other integration test suites will write their results back to the database, where you can check the results.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"rules": {
3+
"dbTests": {
4+
"$testId": {
5+
"adminOnly": {
6+
".validate": false
7+
}
8+
}
9+
},
10+
".read": "auth != null",
11+
".write": true
12+
}
13+
}

integration_test/firebase.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"database": {
3+
"rules": "database.rules.json"
4+
}
5+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "functions",
3+
"description": "Integration test for the Firebase SDK for Google Cloud Functions",
4+
"scripts": {
5+
"build": "./node_modules/.bin/tsc"
6+
},
7+
"dependencies": {
8+
"@google-cloud/pubsub": "^0.6.0",
9+
"@types/lodash": "^4.14.41",
10+
"firebase": ">3.6.9",
11+
"firebase-admin": ">4.1.1",
12+
"firebase-functions": "./firebase-functions.tgz",
13+
"loadash": "^0.0.1",
14+
"lodash": "^4.17.2"
15+
},
16+
"main": "lib/index.js",
17+
"devDependencies": {
18+
"typescript": "^2.0.10"
19+
},
20+
"private": true
21+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as functions from 'firebase-functions';
2+
import { TestSuite, expectEq } from './testing';
3+
4+
export const createUserTests: any = functions.auth.user().onCreate(receivedEvent => {
5+
let user = receivedEvent.data;
6+
let testId: string = user.displayName;
7+
console.log(`testId is ${testId}`);
8+
9+
return new TestSuite('auth user onCreate')
10+
.it('should have a project as resource', event => expectEq(
11+
event.resource, `projects/${process.env.GCLOUD_PROJECT}`))
12+
13+
.it('should not have a path', event => expectEq(event.path, undefined))
14+
15+
.it('should have the correct eventType', event => expectEq(
16+
event.eventType, 'providers/firebase.auth/eventTypes/user.create'))
17+
18+
.it('should have an eventId', event => event.eventId)
19+
20+
.it('should have a timestamp', event => event.timestamp)
21+
22+
.it('should not have auth', event => expectEq(event.auth, undefined))
23+
24+
.it('should not have action', event => expectEq(event.action, undefined))
25+
26+
.run(testId, receivedEvent);
27+
});
28+
29+
export const deleteUserTests: any = functions.auth.user().onDelete(receivedEvent => {
30+
let user = receivedEvent.data;
31+
let testId: string = user.displayName;
32+
console.log(`testId is ${testId}`);
33+
34+
return new TestSuite('auth user onDelete')
35+
.it('should have a project as resource', event => expectEq(
36+
event.resource, `projects/${process.env.GCLOUD_PROJECT}`))
37+
38+
.it('should not have a path', event => expectEq(event.path, undefined))
39+
40+
.it('should have the correct eventType', event => expectEq(
41+
event.eventType, 'providers/firebase.auth/eventTypes/user.delete'))
42+
43+
.it('should have an eventId', event => event.eventId)
44+
45+
.it('should have a timestamp', event => event.timestamp)
46+
47+
.it('should not have auth', event => expectEq(event.auth, undefined))
48+
49+
.it('should not have action', event => expectEq(event.action, undefined))
50+
51+
.run(testId, receivedEvent);
52+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as functions from 'firebase-functions';
2+
import { TestSuite, expectReject, expectEq, expectMatches } from './testing';
3+
4+
const testIdFieldName = 'testId';
5+
6+
export const databaseTests: any = functions.database.ref('dbTests/{testId}/start').onWrite(receivedEvent => {
7+
if (receivedEvent.data.val() === null) {
8+
console.log(
9+
'Event for ' + receivedEvent.params[testIdFieldName]
10+
+ ' is null; presuming data cleanup, so skipping.');
11+
return;
12+
}
13+
14+
return new TestSuite('database ref onWrite')
15+
16+
.it('should not have event.app', event => !event.app)
17+
18+
.it('should not give user refs access to admin data', expectReject(event =>
19+
event.data.ref.parent.child('adminOnly').update({ disallowed: 0 })))
20+
21+
.it('should give admin refs access to admin data', event =>
22+
event.data.adminRef.parent.child('adminOnly').update({ allowed: 1 }).then(() => true))
23+
24+
.it('should have a correct ref url', event => {
25+
const url = event.data.ref.toString();
26+
return Promise.resolve().then(() => {
27+
return expectMatches(url, new RegExp(`^https://${process.env.GCLOUD_PROJECT}.firebaseio.com/dbTests`));
28+
}).then(() => {
29+
return expectMatches(url, /\/start$/);
30+
});
31+
})
32+
33+
.it('should have refs resources', event => expectEq(
34+
event.resource,
35+
`projects/_/instances/${process.env.GCLOUD_PROJECT}/refs/dbTests/${event.params.testId}/start`))
36+
37+
.it('should not include path', event => expectEq(event.path, undefined))
38+
39+
.it('should have the right eventType', event => expectEq(
40+
event.eventType, 'providers/google.firebase.database/eventTypes/ref.write'))
41+
42+
.it('should have eventId', event => event.eventId)
43+
44+
.it('should have timestamp', event => event.timestamp)
45+
46+
.it('should not be admin-authenticated', event => expectEq(event.auth.admin, false))
47+
48+
.it('should not have action', event => expectEq(event.action, undefined))
49+
50+
.run(receivedEvent.params[testIdFieldName], receivedEvent);
51+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as functions from 'firebase-functions';
2+
import * as firebase from 'firebase';
3+
import * as admin from 'firebase-admin';
4+
import * as _ from 'lodash';
5+
import { Request, Response } from 'express';
6+
7+
export * from './pubsub-tests';
8+
export * from './database-tests';
9+
export * from './auth-tests';
10+
11+
firebase.initializeApp(_.omit(functions.config().firebase, 'credential')); // Explicitly decline admin privileges.
12+
admin.initializeApp(functions.config().firebase);
13+
14+
export const integrationTests: any = functions.https.onRequest((req: Request, resp: Response) => {
15+
let pubsub: any = require('@google-cloud/pubsub')();
16+
17+
const testId = firebase.database().ref().push().key;
18+
19+
Promise.all([
20+
// A database write to trigger the Firebase Realtime Database tests.
21+
// The database write happens without admin privileges, so that the triggered function's "event.data.ref" also
22+
// doesn't have admin privileges.
23+
firebase.database().ref(`dbTests/${testId}/start`).set({ '.sv': 'timestamp' }),
24+
// A Pub/Sub publish to trigger the Cloud Pub/Sub tests.
25+
pubsub.topic('pubsubTests').publish({ testId }),
26+
// A user creation to trigger the Firebase Auth user creation tests.
27+
admin.auth().createUser({
28+
email: `${testId}@fake.com`,
29+
password: 'secret',
30+
displayName: `${testId}`,
31+
}).then(userRecord => {
32+
// A user deletion to trigger the Firebase Auth user deletion tests.
33+
admin.auth().deleteUser(userRecord.uid);
34+
}),
35+
]).then(() => {
36+
// On test completion, redirect the user to the Firebase RTDB dashboard, to
37+
// see the results stored there.
38+
resp.redirect(`https://${process.env.GCLOUD_PROJECT}.firebaseio.com/testRuns/${testId}`);
39+
});
40+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as functions from 'firebase-functions';
2+
import { TestSuite, expectEq, evaluate } from './testing';
3+
4+
// TODO(inlined) use multiple queues to run inline.
5+
// Expected message data: {"hello": "world"}
6+
export const pubsubTests: any = functions.pubsub.topic('pubsubTests').onPublish(receivedEvent => {
7+
let testId: string;
8+
try {
9+
testId = receivedEvent.data.json.testId;
10+
} catch (e) {
11+
/* Ignored. Covered in another test case that `event.data.json` works. */
12+
}
13+
14+
return new TestSuite('pubsub onPublish')
15+
.it('should have a topic as resource', event => expectEq(
16+
event.resource, `projects/${process.env.GCLOUD_PROJECT}/topics/pubsubTests`))
17+
18+
.it('should not have a path', event => expectEq(event.path, undefined))
19+
20+
.it('should have the correct eventType', event => expectEq(
21+
event.eventType, 'providers/cloud.pubsub/eventTypes/topic.publish'))
22+
23+
.it('should have an eventId', event => event.eventId)
24+
25+
.it('should have a timestamp', event => event.timestamp)
26+
27+
.it('should not have auth', event => expectEq(event.auth, undefined))
28+
29+
.it('should not have action', event => expectEq(event.action, undefined))
30+
31+
.it('should have pubsub data', event => {
32+
const decoded = (new Buffer(event.data.data, 'base64')).toString();
33+
const parsed = JSON.parse(decoded);
34+
return evaluate(parsed.hasOwnProperty('testId'), 'Raw data was: ' + event.data.data);
35+
})
36+
37+
.it('should decode JSON payloads with the json helper', event =>
38+
evaluate(event.data.json.hasOwnProperty('testId'), event.data.json))
39+
40+
.run(testId, receivedEvent);
41+
});

0 commit comments

Comments
 (0)